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:
- Paging + bcalls — how code and data beyond 64 KiB are reached. (see 02-paging.md, 03-bcall-mechanism.md)
- The floating-point engine — 9-byte BCD reals/complex in the OP1–OP6 registers; all math flows through these. (06-floating-point.md)
- The variable system (VAT) — named objects (reals, lists, matrices, strings, programs, appvars…) catalogued in the Variable Allocation Table. (05-variables-vat.md)
- 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.
| Doc | Subsystem |
|---|---|
| 01-memory-map.md | Address space, ports, RAM layout |
| 02-paging.md | Flash/RAM banking (ports 6/7) |
| 03-bcall-mechanism.md | rst 28h system calls + jump table |
| 04-interrupts.md | IM1 ISR, timers, APD, ON key |
| 05-variables-vat.md | Variable Allocation Table, object types |
| 06-floating-point.md | BCD float format, OP registers |
| 07-tokenizer-basic.md | Token tables, parser/interpreter |
| 08-display-lcd.md | LCD ports, screen buffers |
| 09-keyboard-link.md | Keypad scan, link protocol |
| 10-subsystem-map.md | bcall API surface, system through-line |
| 11-boot-contexts-errors.md | Boot, context system, _JError/onSP |
| 12-memory-management.md | RAM heap, VAT/userMem, Flash archive/GC |
| 13-flash-page-map.md | What each of the 64 flash pages contains |
| 14-ram-pages.md | RAM page selectors, page 83, and restore rules |
| 99-open-questions.md | Prioritized future-work roadmap |
| sub-calculation.md | Calculation engine: FP ops, transcendentals, formatting, errors |
| sub-graphing.md | Graphing: window vars, coord↔pixel, draw primitives, Y= eval |
| sub-tibasic.md | TI-BASIC: program execution, control flow, I/O commands |
| sub-tibasic-tracing.md | TI-BASIC fixture traces, smoke runner, coverage anchors |
| sub-vat-archive.md | Variables, Sto/Rcl, Archive/Unarchive, Flash GC |
| sub-apps-mem-settings.md | Apps find/launch, RAM-reset, MODE/format flags |
| sub-statistics.md | STAT: 1/2-var, regressions, statVars |
| sub-matrix-list.md | Matrix/list element access, Gauss-Jordan inverse/det, matmul |
| sub-solver-numeric.md | Solver root-finder, nDeriv/fnInt, TVM finance |
| sub-table-yvars.md | TABLE generation/cache, Y= equation vars |
| sub-equation-display.md | Equation display / MathPrint layout (page 0x39 eqdisp_*) |
| sub-link-transfer.md | Link protocol: byte/packet/var-transfer (page 0x3C) |
| sub-usb-asic.md | USB 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 | ~bcalls | Representative entry points |
|---|---|---|
| Floating-point / numeric | ~230 (+~120 more in “misc”) | _FPAdd,_Times2,_DivHLBy10,_Intgr,_Trunc,_DToR,_RToD,_Min,_Max,_SqRoot |
| Display / LCD | ~41 | _PutMap,_PutC,_PutS,_DispHL,_NewLine,_ClrLCDFull,_VPutS,_GrBufCpy |
| Variables / VAT | ~37 | _FindSym,_ChkFindSym,_CreateReal,_CreateStrng,_CreateAppVar,_DelVar,_InsertMem,_Arc_Unarc |
| String / convert | ~18 | _ExpToHex,_OP1ExpToDec,_CreateStrng,_StrCopy,_Get_Tok_Strng |
| Parser / TI-BASIC | ~18 | _IsA2ByteTok,_GetTokLen,_BinOPExec,_ParseInp |
| Link / I-O | ~15 | _SendAByte,_RecAByteIO,_SendVarCmd,_Rec1stByte,_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)
- Interrupt keeps time, scans the keypad into
kbdScanCode, runs APD. _GetKeyturns scan codes into key codes (TIKeyCode), driving menus and the homescreen.- The parser reads tokenized input/programs, dispatching each
TIToken. - Number tokens → FP engine (OP1–OP6, BCD); name tokens → VAT (
_FindSym). - Results land in
OP1and are rendered by the display subsystem. - bcall + paging is the substrate that lets steps 3–5 live on different flash pages; errors unwind via
_JError/onSP.
See per-subsystem docs 01–09 for detail.
Conventions & methodology
How to read these notes, and how they were produced.
Suggested reading order
- Overview — the four pillars and the system through-line.
- Subsystem map — see the whole API surface at once.
- Substrate: Memory map → Paging → The bcall mechanism → Interrupts.
- Pick a core subsystem (Floating-point, VAT, Tokenizer/TI-BASIC, Display…), then its feature deep-dive (
sub-*). - Glossary for any unfamiliar term.
Address notation
pp:addr— flash pagepp(00–3F), logical addressaddr. Banked pages run in the4000–7FFFwindow, so e.g._PutSat01:5C39means page 1, address0x5C39. Example:3D:6745.ram:addr— page 0 (the always-mapped kernel) and the RAM window; Ghidra keeps page 0 in itsramspace, soram:229E≡00:229E.- Ghidra’s overlay space writes flash addresses as
page_pp:addr(e.g.page_38:4000); the wiki normalizes these to the shortpp:addrform, sopage_38:4000is written38:4000. - A bare
0x….(no page) is a RAM data address or an unpaged value (e.g.flags0x89F0, the bcall-ID ranges0x4xxx/0x8xxx, a page number like0x3B). - bcall ID ≠ address. A bcall has an ID (the 2-byte word after
rst 28h, 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:
| Flag | Meaning |
|---|---|
| [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 (fromti83plus.inc, the full 2007 TI-83 Plus SDK equates file, or the TI SDK), e.g._FindSym,_FPAdd. High confidence.snake_case— a name inferred 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 at4000), 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 the0x4xxxtable — more known bcall IDs resolve to a valid(addr, page)entry there than on any other page — and is confirmed by the documented RST shortcuts (all six matched) and by every entry resolving and live-confirming once 0x3B is applied.0x8xxxbcall IDs index a retail boot table on page3F(with the USB boot entries on retail page2F). Thisrom.binis a BootFree image — page3Fcarries the BootFree prefix3E 3F D3 06 …and page2Fis blank (allFF) — so these0x8xxxbody targets do not resolve and are left unnamed (tools/bcalls8x_targets.txthas 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, theCALL 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
| Term | Meaning |
|---|---|
| bcall | “branch call” — the OS system-call mechanism: rst 28h + a 2-byte ID, dispatched through a jump table to a routine on any flash page. See The bcall Mechanism. |
| bjump | OS-internal cross-page jump: CALL cross_page_jump; .dw addr; .db page (a tail-jump). The sibling of bcall for the OS’s own use. |
| RST shortcut | A 1-byte rst NN vector that fast-paths a hot routine (rst 10h=_FindSym, rst 30h=_FPAdd, rst 28h=the bcall dispatcher). |
| context | The active “mode” (homescreen, Y= editor, graph, an app…). A block of handler vectors at cxMain (0x858D); the main loop runs the current context’s handlers. See Boot, Contexts & Errors. |
| paging / banking | The Z80 sees 64 KiB; ports 6/7 swap which 16 KiB flash/RAM page is visible in the two middle slots. See Paging. |
| APD | Auto Power Down — the timer-driven idle shutoff. |
| MathPrint | The 2D “pretty-print” rendering of expressions; on this OS the engine is on page 0x39. |
Floating point
| Term | Meaning |
|---|---|
| BCD | Binary-Coded Decimal — numbers stored as decimal digits (2 per byte), the format of all TI floats. |
TIFloat | The 9-byte float: 1 type/sign byte, 1 biased exponent, 7 bytes = 14 BCD mantissa digits. See Floating-Point Engine. |
OP1–OP6 | The six 11-byte floating-point accumulator registers in RAM at 0x8478+. OP1 is the primary accumulator; binary ops use OP1+OP2, result in OP1. |
| FPS | Floating-Point Stack — a software stack (pointer at 0x9824) for spilling OP registers during nested evaluation. |
| guard digits | The 2 extra mantissa bytes past the 9-byte number (OP1EXT/OP2EXT), used for rounding during math. |
Variables & memory
| Term | Meaning |
|---|---|
| VAT | Variable Allocation Table — the RAM catalog of every named object, growing down from symTable (0xFE66). See Variables & the VAT. |
| object type | The 1-byte type tag of a variable (RealObj=0, ListObj=1, ProgObj=5, AppVarObj=0x15…), modeled as the TIVarType enum. |
| archive | Variables relocated to flash to save RAM; the VAT entry’s page byte then points into flash. See Variables, Archive & Unarchive. |
| garbage collection | Compacting 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 heap | The dynamic region from userMem (0x9D95) up to the VAT; managed by _InsertMem/_DelMem. See Memory Management. |
Registers & RAM symbols
| Symbol | Addr | Meaning |
|---|---|---|
IY | (reg) | Held at flags (0x89F0) almost everywhere, so (IY+off) indexes the SystemFlags bitfield. |
flags | 0x89F0 | The IY-indexed system flag area (SystemFlags struct). |
OP1 | 0x8478 | Primary FP accumulator. |
FPS | 0x9824 | Floating-point stack pointer. |
onSP | 0x85BC | SP saved at context/parse start; _JError unwinds to it (try/catch). |
symTable | 0xFE66 | Top of RAM; the VAT grows down from here. |
kbdScanCode | 0x843F | Last keypad scan code (filled by the ISR, read by _GetCSC). |
plotSScreen | 0x9340 | The 768-byte graph/display buffer (96×64). |
parsePtr / parseEnd | 0x965D / 0x965F | The TI-BASIC parser’s token-stream cursor. |
Conventions
- Addresses: written
pp:addrwhereppis the flash page (00–3F) — e.g.3D:6745. Page 0 (the always-mapped kernel) is also writtenram:addrsince Ghidra keeps it in theramspace. A bare0x….with no page is a RAM/data address. See Conventions. - bcall IDs vs addresses: a bcall has both an ID (the 2-byte value after
rst 28h, e.g._FlashToRam=5017h) and a body address (3D:6745). The ID is not an address. - Confidence flags:
[confirmed](seen in disassembly),[standard](matches documented TI-83+/84+ behavior),[hypothesis](inferred). See Conventions. - Function names: official TI bcalls are
_CamelCase(_FindSym); RE-inferred names aresnake_case(findsym_scan).
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)
| Range | Slot | Contents | Notes |
|---|---|---|---|
0000–3FFF | Flash bank 0 | Flash page 0 (fixed) | Boot/kernel: RST vectors, dispatcher, FP/VAT core. Never swapped. [confirmed] |
4000–7FFF | Flash bank A | Swappable 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. _JErrorNo→00:2799). [confirmed] |
8000–BFFF | Bank B | Swappable RAM/flash page (port 7) | Usually RAM. [standard] |
C000–FFFF | RAM | RAM 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) | Role | Evidence |
|---|---|---|
00 | Boot/kernel core, mapped at 0000 | RST vectors, bcall_dispatcher, FP/VAT/mem routines [confirmed] |
01 | OS routines (display, homescreen text, menus) | _PutC,_PutS,_ClrLCDFull,_NewLine resolve here [confirmed] |
06 | OS routines (key input, parser-ish) | _GetKey→06:491E [confirmed] |
2F | USB boot support page | supplied by local D84PBE2.8Xv; retail page 3F maps _AttemptUSBOSReceive→2F:4145, _ReceiveOS_USB→2F:48CA, _InitUSB→2F:52A4, _KillUSB→2F:5961 [confirmed] |
3B | bcall jump table | highest-scoring page for the 0x4xxx bcall ID table; first entry _JErrorNo→00:2799 [confirmed] |
3C | Link code + OS version string ("2.55MP") | page starts 32 2E 35 35 4D 50 [confirmed] |
3E | Certification 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] |
3F | Retail boot page | supplied 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)
| Addr | Name | Type | Purpose |
|---|---|---|---|
0x8478–0x84B9 | OP1–OP6 | TIFloat slot (9B body + 2B …EXT guard, 11B-spaced) | Floating-point accumulators [confirmed] |
0x89F0 | flags | SystemFlags (74B) | IY-indexed system flag bitfield [confirmed] |
0x844B/0x844C | curRow/curCol | byte | Homescreen text cursor (16 cols) [confirmed] |
0x8447 | contrast | byte | LCD contrast [confirmed] |
0x843F/0x8444 | kbdScanCode/kbdKey | byte | Last key scan code / key [confirmed] |
0x9340 | plotSScreen | byte[768] | Graph/display buffer (96×64/8) [confirmed] |
0x86EC | saveSScreen | byte[768] | Saved screen buffer [confirmed] |
0x9824 | FPS | — | Floating-point stack pointer [standard] |
0x85BC | onSP | — | SP saved by ON-interrupt [confirmed] |
IY is held at flags (0x89F0) almost everywhere, so (IY+off) accesses index SystemFlags fields (appFlags, kbdFlags, …).
Principal 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).
| Port | Name | Purpose |
|---|---|---|
00 | link | Link port lines |
01 | keypad | Keyboard matrix select/read |
02 | hwStatus | Status (bit7 used at reset) |
03 | intMask | Interrupt enable mask |
04 | intStatus / memMapMode | Read = interrupting-device ID + ON-held; write = memory-map mode + timer rate |
05 | mapBankC | RAM page in slot C000 (MemC) on the 84+ |
06 | mapBankA | Flash page in slot 4000 |
07 | mapBankB | Page in slot 8000 (0x81=84+ mode seen in ISR) |
08–0D | usb/link assist | 84+ hardware byte-assist control/status/data/FIFO ports; see USB ASIC and link assist |
10/11 | lcdCmd/lcdData | LCD controller |
20 | cpuSpeed | 0=6 MHz, 1=15 MHz (set in ISR) |
21 | asicVer/ramSize | ASIC 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) |
4D | usbLineState | USB line-state gate sampled by _GetVarCmdUSB (id 50FB; Ghidra alias _LinkXferOP); bits 5/6 gate the ram:2E0B bjump to 35:4280 |
55/56 | usbIntStatus/usbLineEvents | USB 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.
| Slot | Select port | Selects |
|---|---|---|
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 at4000+, then restore the previous page; bcalls whose body lives on page 0 run it in place below4000(e.g._JErrorNo→00:2799). See 03-bcall-mechanism.md. The helperram:181cis the page-set primitive used by the dispatcher. - A routine that runs banked into
4000must therefore be written position-fixed for4000— which is exactly why every overlay page in Ghidra is based at4000. 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 pagepayload 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:
- Read the 2-byte ID
dwfrom the caller’s return address. - Decode the ID’s high bits:
bit15/bit14select the address class; the low bits form the table offset. - Bank the bcall table page into slot A (via the helper at
ram:181c, which setsport_mapBankA). - Read the 3-byte table entry: target address (2) + target page (1).
- Bank the target page into slot A (
port_mapBankA = page), save the previous page. callthe target. On return, restore the previous page and resume the caller at+3.
The jump table — flash page 0x3B [confirmed]
- Located at the start of physical flash page
0x3B(file offset0x3B*0x4000 = 0xEC000). - 3-byte entries:
addr_lo, addr_hi, page. IDs step by 3 from0x4000, so entry for ID X is at table offsetX-0x4000. - Resolution method (
tools/Python): scored all 64 pages by how many of the named .inc IDs produced a valid(addr∈4000..7FFF or page-0, page<0x40)entry; page0x3Bscored highest (the page-selection heuristic uses a conservative validity filter chosen only to pick the table). Once0x3Bis selected and applied, all 535.incIDs plus 61 RE-named entries (596 total) resolve and are live-confirmed. - Validation: known bcalls land exactly where expected —
_PutS→01:5C39,_GetKey→06:491E,_ClrLCDFull→01:60E4,_GetCSC→00:04B2,_CreateReal→00: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:
0x4xxx–0x7FFF(bit 14 set): the main table on flash page0x3B, entry at offsetID − 0x4000(596 live-confirmed bcalls: 535 from the.inc+ 61 RE-named).0x8xxx(bit 15 set): the retail boot table is on physical page3F, indexed byID & 0x7FFF.D84PBE1.8Xvsupplies the retail page3F;D84PBE2.8Xvsupplies the companion USB boot support page2F. Most entries resolve to3F:addr; USB entries such as_AttemptUSBOSReceive(80E4) and_InitUSB(8108) resolve to2F:addr.tools/resolve_bcalls.pyrefuses to emit these targets from a BootFree-substituted page. [confirmed]
Both resolved table formats are 3-byte entries: target address (little endian) plus page byte masked with & 0x3F.
RST shortcuts (fast inlined bcalls) [confirmed]
Five of the RST vectors are 1-byte fast paths for the hottest routines (each JPs to its page-0 handler, which is also reachable as a bcall — the table maps the same address). rst 28h is the bcall dispatcher itself (not a shortcut, and ram:2A2F is not a bcall target); it is listed here only to complete the vector set:
| Opcode | Vector → target | Routine |
|---|---|---|
rst 08h | 0008→1A2F | _OP1ToOP2 (copy FP reg) |
rst 10h | 0010→0E65 | _FindSym (VAT lookup) |
rst 18h | 0018→155C | _PushRealO1 (push OP1 to FPS) |
rst 20h | 0020→1B01 | _Mov9ToOP1 (copy 9 bytes → OP1) |
rst 28h | 0028→2A2F | bcall dispatcher |
rst 30h | 0030→229E | _FPAdd (float add) |
All six match the documented TI-83+/84+ RST assignments — strong cross-confirmation of the table resolution.
bjump — the sibling mechanism (OS-internal cross-page calls)
Besides bcalls, the OS calls its own cross-page routines via bjump: 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 usescross_page_jumpto 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:
port_usbIntStatus(0x55) — the 84+ USB Interrupt State port. This OS overloads it as the ISR’s master “anything pending?” gate:(val ^ 0xFF) & 0x1Ftests the 5 active-low sources.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.)- 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.
- ON key — sets an ON-flag;
- Hardware-mode housekeeping: checks
port_mapBankB == 0x81(84+ mode), and on one path setsport_cpuSpeed = 1(15 MHz) andport_mapBankB = 0x81. - 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) | bit | field / equate | meaning in the ISR |
|---|---|---|---|
IY+0x03 | 1 | flag byte 0x03 bit1 | ON-key interrupt already latched (guards the ON-set path @ ram:00F5) |
IY+0x03 | 0 | graphFlags·graphDraw | redraw-graph flag the ISR sets @ ram:0109 |
IY+0x08 | 2 | apdFlags·apdAble | APD enabled; toggled by _DisableApd/_EnableApd |
IY+0x09 | 3 | onFlags·onRunning | calculator-running flag; tested before the 84+ USB-port path (ram:008B, ram:099E) |
IY+0x09 | 4 | onFlags·onInterrupt | ON-key interrupt-request flag; set @ ram:0A87 |
IY+0x0C | 3 | curFlags·curOn | cursor currently drawn (blink phase) |
IY+0x0C | 2 | curFlags·curAble | cursor-blink enabled (curLock is bit 4) |
IY+0x0F | 7 | seqFlags bit7 | cleared @ ram:0A8C (RES 7,(IY+0Fh)) on the ON-key path |
IY+0x12 | 3 | shiftFlags·shift2nd | the [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+0x12 | 0 | indicFlags·indicRun | run-indicator-on flag (set by _RunIndicOn); the byte is shared — bits 0–2 are indicFlags, bits 3–7 are shiftFlags |
IY+0x16 | 0 | speed/ACK select | chooses the value re-written to int-mask port 0x03 on exit (ram:00E6) |
IY+0x16 | 1 | (same byte) | link-busy sub-flag, reset @ ram:015E |
IY+0x24 | 2 | link/transfer-active | guards the ON-break vs. link-restore decision (ram:09EE, ram:0AAB) |
IY+0x28 | 7/3 | APIFlg·appRetKeyOff (b7) | ISR tests BIT 7 (appRetKeyOff) @ ram:09DB and does SET 3 @ ram:09E1 on the ON-break path |
IY+0x2C | 0 | mouseFlag1 bit0 | scanner-active flag tested by kbd_scan_autorepeat @ ram:0415 (the scan code itself is RAM kbdScanCode 0x843F, not an IY flag) |
IY+0x33 | 5/0 | context-restore sub-flags | branch selectors on the ON-break / restore path |
IY+0x3A | 0 | hookflags5·usbActivityHookActive | when set, the ISR runs the deferred USB-activity hook (ram:032A) and ACKs |
IY+0x3F | 7 | RAM-clear control | masked during the ON-key RAM wipe (ram:0B3C) |
IY+0x44 | 2 | (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.
Timer reprogramming, APD timeout, and cursor blink [confirmed mechanism; exact tick value [hypothesis]]
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 constant0x74to0x8449(apdTimer). The per-tick decrement is in page 0 atram:036C(LD HL,0x8448; DEC (HL); RET NZ; INC HL; DEC (HL)— decrementapdSubTimer, and on its underflowapdTimer); 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 theram:3FB1bjump) for timer 3 / port0x37, and33:5EB4(viaram:3FB7) for timer 1 / port0x31— 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 siteram:036Cconfirmed; tick rate / absolute seconds [hypothesis]]- The unrelated
indicCounter/indicBusypair (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 atram:027B(dec_apd_timerin the symbol map, though it decrements the run-indicator counter) doesDEC (0x8476); RET NZeach 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 with0x32(50) (LD A,0x32; LD (0x844A),A).- The ISR tests
curAble(IY+0x0Cbit 2) atram:019Band, if enabled, calls the cursor tick through theram:3FCFbjump to06:7C45, which decrements0x844A; on underflow it togglescurFlags(IY+0x0C) bit 3 (curOn) to flip the glyph and reloads0x32. 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 value0x32, counter0x844A, and tick site06:7C45confirmed; 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)&0x1Fmasks 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 mask0x03is 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 usesindicCounter/indicBusy(0x8476/0x8477), seeded by_RunIndicOn. [confirmed] _GetCSC(00:04B2) cooperates with the ISR: the ISR (or keypad path) updateskbdScanCode;_GetCSCatomically 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 / port0x37, viaram:3FB1) and33:5EB4(timer 1 / port0x31, viaram: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 0x80B7 → 3F:477C) and _getHardwareVersion (bcall 0x80BA → 3F:4781). The USB boot support entry points route through the same table but land on page 2F, for example _AttemptUSBOSReceive (0x80E4 → 2F:4145) and _InitUSB (0x8108 → 2F:52A4).
RAM clear / re-init (ram_reset_wipe → ram:0BD9) [confirmed]
The RAM-init proper is ram_reset_wipe (35:719F, reached on a full reset; the same routine backs the [2nd]+[+] · 7 · 1 · 2 RAM-reset and the post-boot RAM clear). It zero-fills RAM in two blocks, preserving a handful of flag bits and 0x9B73 across the wipe:
ram_reset_wipe (35:719f):
; save flags to preserve: (9B73), (IY+34).6, (IY+35).0, (IY+35).1, (IY+3F)&0x7F
DI
LD HL,0x8000 ; LD DE,0x8001 ; LD BC,0x1BC3 ; LD (HL),0 ; LDIR ; clear 8000..9BC3
... restore the saved flag bits ...
LD HL,0x9BD0 ; LD DE,0x9BD1 ; LD BC,0x642F ; LD (HL),0 ; LDIR ; clear 9BD0..FFFF
JP 0x0BD9
ram_init_after_reset (ram:0BD9):
LD A,0xC0 ; OUT (0),A ; port 0 = memory-map control
LD SP,0xFFF7 ; reset stack to top of RAM
CALL 0x3EC1 ; continue init (page-0 kernel): VAT, sysflags, LCD …
So RAM is wiped in two LDIR runs (0x8000–0x9BC3, then 0x9BD0–0xFFFF, leaving the 0x9BC4–0x9BCF window and the explicitly-saved flag bytes intact), then ram:0BD9 resets the memory map (port 0) and the stack and hands off through ram:3EC1. This ram:0BD9 entry is the same RAM re-init point cross-referenced from 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 — recallcxCurAppis a key code, so e.g.[GRAPH]→ the graph context).0xFF/0x7F— quit / no-op.
So the router classifies a mode key before the active context sees it and returns a context-switch code (0xFB/0xFC); the caller then swaps the cx* vectors. The router itself only writes keyExtend (0x8446, the extended-key state) — its body holds no store to the cx* block. [confirmed]
Contexts — 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, setsflags.appFlags, and savescxPage = port_mapBankA(the page the app runs from). [confirmed]- The dispatched handlers include things like a key handler, (re)display/paint handler, and a PutAway (suspend) handler — the OS calls them through the
cx*vectors, paging incxPagefirst. _PutAway(ram:08AF) calls the current context’s PutAway handler (cxPPutAway) to suspend/clean up — used on APD, when switching apps, or on2nd+QUIT. [confirmed]
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):
| Off | Addr | Field | Meaning |
|---|---|---|---|
| +0 | 858D | cxMain | main/event handler ptr |
| +2 | 858F | cxPPutAway | putaway handler ptr |
| +4 | 8591 | cxPutAway | putaway |
| +6 | 8593 | cxRedisp | redisplay/repaint handler ptr (the inc’s cxRedisp bcall, id 0x4C6C, body ram:08D0, reads this slot via LD HL,(8593) and dispatches it) |
| +8 | 8595 | cxErrorEP | error entry point ptr |
| +10 | 8597 | cxSizeWind | window-size handler ptr |
| +12 | 8599 | cxPage | flash page the handlers live on |
| +13 | 859A | cxCurApp | current context id — equals a key code (cxGraph=kGraph, cxCmd=kQuit, cxPrgmEdit=kPrgmEd …) |
| +14 | 859B | cxPrev | base of the 14-byte shadow of cxMain…cxCurApp (plus a separately-saved appFlags byte) — the suspended previous context |
_AppInit copies the 6 vectors (12 bytes, +0..+11) from an app’s header into this block, then sets cxPage. Because cxCurApp is a key code, a mode-switch key naturally selects the context to load.
The full _AppInit body confirms the offsets directly — HL points at the app’s 12-byte vector header, LDIR lands them at cxMain=0x858D, and the byte that follows the 12 vectors becomes a flags byte; cxPage is then loaded from the live bank-A page-select (port 6), not copied from the header:
_AppInit (ram:0936):
; HL = source (12-byte vector header) on entry
LD DE,0x858D ; -> cxMain
LD BC,0x000C ; 12 bytes = the 6 handler vectors
LDIR ; cxMain..cxSizeWind+1 (+0..+11)
LD A,(HL) ; the 13th header byte (appFlags)
LD (0x89FD),A ; -> appFlagsAddr (system flag byte)
IN A,(0x6) ; current bank-A flash page
LD (0x8599),A ; -> cxPage (+12, the page the handlers run from)
RET
The destination 0x858D and length 0x000C pin the six 2-byte handler slots cxMain(+0) cxPPutAway(+2) cxPutAway(+4) cxRedisp(+6) cxErrorEP(+8) cxSizeWind(+10), and the explicit LD (0x8599),A writes cxPage at +12 from port 6. _AppInit installs a context, but it is not the only writer: _POPCX (bcall 0x49E1, body 07:6D1C) restores a suspended context by LDIRing 14 bytes cxPrev→cxMain (0x859B→0x858D) and copying a 15th byte into the app-flags, and a matching save path (the LDIR at 07:5A8C) copies cxMain→cxPrev. cxCurApp(+13, 0x859A) is the current context id (a key code); the shadow at cxPrev(0x859B) holds the suspended context.
How a context handler is invoked [confirmed]
call_context_main (ram:08fa): set_bankA_page(cxPage); call (cxMain) via jp_hl; ret ; 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 inA(theTIErrorenum:E_Domain,E_DivBy0,E_Memory, … each ORed withE_EDIT=0x80 if re-editable)._JErrorstores the code toerrNo(0x86DD); the sibling entry_JErrorNo(ram:2799) raises the already-storederrNowithout taking a new code. - The handler restores the stack from
errSP(0x86DE,LD SP,(errSP)atram:27BB), restores a sane state, and displays the error screen (ERR:+ message, with1:Quit 2:Goto).errSPis the current error frame;_resetStacksseeds it fromonSP(0x85BC, the context-level saved SP) at context/parse start. - The
E_EDITbit (0x80) tells the handler the error is editable (offer “2:Goto” to jump to the offending token).
So errSP + _JError together implement try/catch: a context seeds errSP (from onSP) at entry, and any depth of nested calls can abort straight back to it.
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:
| Code | TIError | Message @ page_07 |
|---|---|---|
| 1 | E_Overflow | OVERFLOW (6B3C) |
| 2 | E_DivBy0 | DIVIDE BY 0 (6B45) |
| 3 | E_SingularMat | SINGULAR MAT (6B51) |
| 4 | E_Domain | DOMAIN (6B5E) |
| 5 | E_Increment | INCREMENT (6B65) |
| 6 | E_Break | BREAK (6B6F) |
| 7 | E_NonReal | NONREAL ANS (6B75) |
| 8 | E_Syntax | SYNTAX (6B81) |
| 9 | E_DataType | DATA TYPE (6B88) |
| 10 | E_Argument | ARGUMENT (6B92) |
| 11 | E_DimMismatch | DIM MISMATCH (6B9B) |
| 12 | E_Dimension | INVALID DIM (6BA8) |
| … | … | UNDEFINED, MEMORY, INVALID, ILLEGAL NEST, BOUND, WINDOW RANGE, ZOOM, LABEL, STAT, SOLVER, … 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 andcxPageoffsets are pinned by tracing_AppInit(ram:0936):LD DE,0x858D / LD BC,0x000C / LDIRthenIN A,(6) / LD (0x8599),A. See Context block layout above for the full offset table and_AppInitbody._AppInitinstalls the block; it is not the sole writer —_POPCX(bcall0x49E1→07:6D1C) restores a saved context intocxMain, and a save path at07:5A8CcopiescxMaininto thecxPrevshadow.- Boot RAM-init trace — raw-disassembly trace. Reset (
ram:0000) →028cpaging setup →JP 0x812c(boot page3F:412C— BootFree substitute in thisrom.bin; retail boot inD84PBE1.8Xv). The RAM clear/re-init isram_reset_wipe(35:719f): twoLDIRzero-fills (0x8000–0x9BC3,0x9BD0–0xFFFF) preserving a few flag bytes, thenJP 0x0BD9(ram_init_after_reset: port 0 =0xC0, stack reset in the raw trace,CALL 0x3EC1). Theram:0BD9entry matches the re-init point cross-referenced in 12-memory-management. See RAM clear / re-init. - Flash write/erase sector primitives. Page-3D anchors include
flash_program_buf3D:678C, the per-record status writers3D:7C8F/7C93/7C97, andflash_erase_wait3D:5ED3, with byte-poke loops copied toramCode0x8100. The candidate labelsflash_program_core3D:61AFandflash_write_record3D:64AAare not defined functions in the disassembly; both names are project-local inferred labels, not WikiTI orti83plus.incequates. The public single-byte flash writers are_WriteAByte(bcall0x8021) and_WriteAByteSafe(bcall0x80C6) inti83plus.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]:
| Ptr | Addr | Role |
|---|---|---|
tempMem | 0x9820 | base of the temporary area |
fpBase | 0x9822 | floating-point stack base |
FPS | 0x9824 | FP stack pointer (grows; _PushReal/_PopReal) |
OPBase | 0x9826 | base of OP/symbol scratch |
OPS | 0x9828 | OP/symbol scratch stack pointer (top) |
pTemp | 0x982E | temp-variable pointer |
progPtr | 0x9830 | currently-executing program pointer |
pagedBuf | 0x983A | paged scratch buffer |
_MemChk reports free RAM as (OPS) − (FPS) (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 ofHLbytes at addressDEby shifting all memory above it up. It callsinsertmem_setup(ram:0F8B), which does theLDDRblock move (atram:0FA1), thendelmem_fixup_tail(ram:1398) to fix up pointers._InsertMemdoes not check free space itself — callers must ensure room first via_EnoughMem(the wrapper_ErrNotEnoughMematram:1735calls_EnoughMemthen jumps to_ErrMemoryatram:2721on shortfall)._DelMem(ram:1368) — the inverse: close a gap, shifting memory down._EnoughMem(ram:0FA6) — ensure N free bytes; if short, it walks the temp/scratch entries (9-byte stride frompTempdown toOPBase) and_DelVars reclaimable temporaries to make room. [confirmed]_MemChk(ram:0E20) — compute current free RAM.
Variable-creation bcalls — _CreateReal, _CreateStrng, _CreateAppVar, _CreateRList, 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(id5017→ body3D: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 olderflash_gc_relocate@3C:7BD0/gc_show_screen@3C:7E0Dlabels 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 helpersflash_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, and3D:6413are 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_Unarcdispatches on the FindSym page byteB:B==0/in-RAM →6107archive,B≠0/in-Flash →61F4unarchive.)
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.
| Addr | Field (this doc’s name) | Meaning |
|---|---|---|
83EE | arcInfo.page | page byte of the data (Flash page if archived; RAM marker otherwise) |
83EF | arcInfo.dataPtr | 2-byte data address (in Flash window 0x4000–0x7FFF, or RAM) |
83F1 | arcInfo.vatPtr | pointer to the VAT entry’s type byte (the symbol record) |
83F3 | arcInfo.destPtr | destination data pointer (RAM target on unarchive) |
83F5 | arcInfo.dataSize | a header/record-size component (loaded from BC after CALL 0FDE) |
83F7 | arcInfo.size | the variable’s data byte count (from _DataSize; 614B does CALL 1485 → LD (83F7),DE) |
83F9 | arcInfo.sizeFull | size + header overhead |
8406 | savedArcInfo | 12-byte save slot for nested calls |
RAM-heap pointers used by the mem checks (cluster at 0x9820–0x983A, confirmed in .inc):
FPS=9824, OPBase=9826, OPS=9828 (top of the upward data heap), pTemp=982E,
progPtr=9830. The VAT grows down from symTable=0xFE66. chkDelPtr3=981C holds the result
pointer from the last lookup (_Arc_Unarc does LD (981C),HL) — note 981C is chkDelPtr3 in
ti83plus.inc, not tSymPtr1 (which is 9818h). ramCode=8100h is where Flash
read/write routines are copied to run (you cannot execute from a Flash page while erasing it).
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 B — B is the discriminator: zero for an in-RAM var, nonzero for a var
whose data lives on a Flash page.
VAT entry shapes (consistent with _CreateR* header writes — see 05-variables-vat.md):
- single-char (real/cplx/
Ln/[A]/sysvars): high-address-first name token, page byte, data pointer, and type byte;findsym_scanreads 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. 5F45resolves/creates the target symbol; then it copies the value. It dispatches on the destination name token (849B): list-element store (0x2A→ bounds-checks via_ErrDimension), matrix element, etc. Ultimately a_Create*routine carves RAM with_InsertMemand the data is copied.- A store into an archived var is not done in place; the OS unarchives first (you cannot rewrite
Flash in place) — see the
_Arc_Unarcdirection logic in §4. [hypothesis]
Recall _RclVarSym (38:67B1) and _RclVarPush (3A:5D07):
_RclVarSymcallsRST 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_LdHLindand cross-page helpers; endsJP _OP4ToOP1._DataSize(00:1485): returns the variable’s data byte-count in DE from the type byte — real=9, list/cplx-list read theword countheader, matrix uses cols×rows, and named types (0x15AppVar,0x16,0x17Group) read the leadingword size.- 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 the2729(E_Invalid,0x8F) entry on overflow.26E0+ is a cluster of local error shims: each loads its code (0xB2=E_Variable,0xB3=E_Duplicate,0x81=E_Overflow,0x82=E_DivBy0) intoAand enters_JError— not_ErrDataType.- Error-name strings live at
07:6CA9:ARCHIVED, VERSION, ARCHIVE FULL, VARIABLE, DUPLICATE.
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 (_WriteAByte→3F:4C9F,
_WriteAByteSafe→3F:4C9A, _FlashToRam2→3F: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:addr | Role |
|---|---|---|
00:2FF1 | 3D:64AA (inferred label) | Flash program record candidate |
00:2FDF | 3D:61AF (inferred label) | Flash program/erase core candidate |
00:2FF7 | 3D:62C2 (inferred label) | Flash free-sector scan / allocate candidate |
00:2FC1 | 3C:580E | Flash command/menu entry |
00:2FFD | 3C:7121 (inferred label) | Flash command dispatcher candidate |
00:32A9 | 05:4A6E | complex-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; …):
| Routine | Mask in C | Bit cleared | State after |
|---|---|---|---|
flash_op_fe (3D:7C97) | 0xFE | bit 0 | record in-progress (newly begun) |
flash_op_fd (3D:7C8F) | 0xFD | bit 1 | (intermediate / “swap” marker) |
flash_op_fb (3D:7C93) | 0xFB | bit 2 | (intermediate) |
Successive clears compose: the three helpers take a record 0xFF (erased) → 0xFE (started) →
0xFC (valid/complete, bits 0+1 clear). Deletion marks the record 0xF0 (deleted/dead, bits
0–3 clear) with a direct write in the delete/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 test | Archive base page (flash_page_select 3D:726E) | Archive top page (flash_cmd_base 3D:738B) | Page mask |
|---|---|---|---|
| port 2 bit 7 clear (1 MB) | 0x15 | 0x1E | AND 0x1F (32 pages) |
| port 0x21 == 0 (mid) | 0x29 | 0x3E | AND 0x3F (64 pages) |
| else (2 MB) | 0x69 | 0x7E | no 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:5EF1 — 3D: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 at01:4126;"Defragmenting...\0"at01:4076. The display front-end candidate3C: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 7E0DthenCALL 7219thenCALL 7733),3C:720D= relocate-only, and the archive-full auto-GC3C:7204runs71FC(GC) then retries the write at7F1C.3C:7121is an inferred label, not byte-confirmed in the disassembly. - The relocation/erase-core candidate
3C:7BD0–7BF4: tests a status flag,7E6B/7C10prepare the swap sectors (writes0xF0marker, sets97A6sector counter,8477),7BE3:CALL 7E0Dshows the banner,7C1Fwalks live VAT/Flash entries copying each valid (0xFC-marked) record to the new sector, and7C04finalizes (erases the old sectors,SET 2,(IY+0x25)). [standard]3C:7BD0is an inferred label, not byte-confirmed in the disassembly; theflash_gc_relocate/gc_show_screennames are project-local inferred labels, not WikiTI orti83plus.incequates. - GC is callable from the user catalog (
Archive/the MEM menu “Garbage Collect?” — string at01: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, elsecount(INC HL⇒ off-by-one inclusive).OPSis the top of the upward data heap; the gap to the downward VAT is the real free RAM (see_InsertMemcollision check). The decompiler’s trivial 2-line view is wrong — the real routine subtracts the two pointers._EnoughMem(00:0FA6) — ensure N free bytes; if short it walks the temp/scratch entries frompTemp(982E)down towardOPBase(9826)at a 9-byte stride, and_DelVars any entry whose flag byte has bit 7 (& 0x80) set (a reclaimable temporary), looping until enough or exhausted. Used by the_Create*routines and by the unarchive RAM-fit check (61F4calls it before allocating)._InsertMem(00:0F81) /_DelMem(00:1368) — open / close a gap at HL by block-moving everything above;_InsertMemfailsE_Memoryif it would collide with the VAT.- Free archive (Flash) is computed inside the Flash layer. The free-space sum is at
3D:6413and the catalog “MEM” read path runs through3C:7121. Neither address is byte-confirmed in the disassembly.
9. Confident address index
| space:addr | name | what |
|---|---|---|
07:6248 | _Arc_Unarc | archive/unarchive entry; toggles arc flag, dispatches RAM↔Flash |
07:628B | arc_chk_name | archivable-name validator |
07:6107 | arc_ram_to_flash | RAM→Flash archive worker (programs Flash, frees old RAM) |
07:61F4 | arc_flash_to_ram | Flash→RAM unarchive worker (carves RAM, copies from Flash) |
07:6331 | arc_size_setup | stash vatPtr, compute dataSize into arcInfo |
07:61DC | arc_save_info | save 12-byte arcInfo into savedArcInfo; 07:61E8 (restore candidate) is an inferred label, not byte-confirmed in the disassembly |
07:565F | findsym_scan | the real _FindSym VAT scanner |
00:0E65 | _FindSym | RST10 trampoline → findsym_scan |
00:0E60 | _ChkFindSym | type-check OP1 then FindSym |
00:1485 | _DataSize | variable data byte-size by type |
38:62A9 | _StoOther | store value into named var |
38:67B1 | _RclVarSym | recall var by symbol |
3A:5D07 | _RclVarPush | recall var, push to FPS |
3D:6745 | _FlashToRam | copy archived data Flash→RAM (page-aware); ti83plus.inc sibling _FlashToRam2 (id 8054) is named but its body is unmapped in the disassembly |
3D:678C | flash_program_buf | live-MCP Flash programming/buffer helper |
3D:64AA | flash_write_record (inferred label) | program an archived record to Flash candidate; not byte-confirmed in the disassembly |
3D:61AF | flash_program_core (inferred label) | Flash program/erase core candidate; not byte-confirmed in the disassembly |
3D:62C2 | flash_alloc_sector (inferred label) | scan/allocate next free archive sector candidate; not byte-confirmed in the disassembly |
3D:6413 | flash_free_scan (inferred label) | sum free archive space / decide GC candidate; not byte-confirmed in the disassembly |
3D:726E | flash_page_select | archive base page by model (0x15/0x29/0x69) |
3D:738B | flash_cmd_base | archive top page by model (0x1E/0x3E/0x7E) |
3D:727D | flash_set_sector_cnt | shared page counter 0x82A3 = base+1 |
3D:5ED3 | flash_erase_wait | erase a 16 KB archive page, wait for completion |
3D:7C97 / 3D:7C8F / 3D:7C93 | flash_op_fe/fd/fb | clear status bit (0xFE/0xFD/0xFB AND-mask) |
3D:7DEA | flash_find_nonff | scan 13-byte header for all-0xFF (free slot) |
00:1837 / 00:182F | probe_hw_model_keep_a / probe_port21_keep_a | model bits: port 2 bit7 / port 0x21 low |
3D:6B9B | flash_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:7121 | flash_cmd_dispatch (inferred label) | Archive/UnArchive/GC command dispatcher candidate; not byte-confirmed in the disassembly |
3C:7BD0 | flash_gc_relocate (inferred label) | GC core candidate; not byte-confirmed in the disassembly |
3C:7E0D | gc_show_screen (inferred label) | “Garbage Collecting…” display front-end candidate; not byte-confirmed in the disassembly |
00:0E20 | _MemChk | free RAM = OPS − FPS |
00:0FA6 | _EnoughMem | ensure N bytes; reclaim temps |
00:0F81 | _InsertMem | open a RAM gap |
00:1368 | _DelMem | close a RAM gap |
00:12D9 | _DelVarArc | delete var incl. archived copy |
00:1308 | _DelVar | delete var + VAT entry |
Strings: 01:4126 “Garbage Collecting…”, 01:4076 “Defragmenting…”, 07:6CA9
“ARCHIVED/VERSION/ARCHIVE FULL/VARIABLE/DUPLICATE”, 01:76C9 “Garbage Collect?”.
Ports: 0x06 = bank-A page select (Flash window), 0x14 = Flash write/erase control,
0x02 bit7 = Flash-size/model. RAM run-from-RAM stub: ramCode = 0x8100.
10. Summary & open items
-
Sector map / erase-block — [confirmed], see §6b. The archive pool is model-selected: base page
0x15/0x29/0x69(flash_page_select3D:726E) up to top page0x1E/0x3E/0x7E(flash_cmd_base3D:738B); on a 1 MB TI-84 Plus that is raw pages ~0x15…0x1E. The OS pages the region into the0x4000window and erases one 16 KB page at a time (flash_erase_wait3D:5ED3, sector counter0x82A3fromflash_set_sector_cnt3D:727D); the physical chip sector is 64 KB = 4 OS pages [hypothesis]. -
Record-status bytes — [confirmed], see §6a. Monotonic bit-clear:
0xFFerased →0xFEin-progress →0xFCvalid viaflash_op_fe/fd/fb(3D:7C97/7C8F/7C93) AND-masking;0xF0deleted is a direct write in the delete/GC path the status byte;flash_find_nonff(3D:7DEA) treats an all-0xFFheader as free. -
Lower-level flash helper bodies — address-keyed labels still inferred [hypothesis]. The public bcall entry points are canonical equates in
ti83plus.incand now resolve through the retail boot table:_WriteAByte(8021) →3F:4C9F,_WriteAByteSafe(80C6) →3F:4C9A, and_FlashToRam2(8054) →3F:4888. The address-keyedflash_*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 (type0x17, like AppVar0x15/0x16) carries a leading word-size header, so a group can be stored as one Flash blob. In_Arc_UnarctheCP 0x17→26E0reject sits on the B≠0 (in-Flash) branch, immediately before the unarchive worker61F4— so an archived group is not unarchived through61F4, 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 entryCALLis not disassembled here (cross-pageCALLflagged 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 nibble | field payload size |
|---|---|
0xD | 1 byte |
0xE | 2 bytes |
0xF | 4 bytes |
3D:7285 AND 0x0F; CP 0x0F -> B=4 ; CP 0x0E -> B=2 ; CP 0x0D -> B=1
The master field at offset 0 is usually 80 0F … (field 800, size nibble F, followed
by a 4-byte size) — this is what the page-scan keys on to recognise an app. Fields carry
the app name, the page count, flags, the date stamp, and signature-related data.
The public header descriptions match the ROM parser and the local app corpus. Useful
references are WikiTI’s
application-header
and certificate/header format
pages, TI’s
AppHeader guide, and
Tari’s Cemetech disassembly note,
which describes .8xk data as Intel HEX pages based at 0x4000 and app code as starting
after field 807.
Common app-header fields in the sample corpus:
| field | meaning | observed payload |
|---|---|---|
800 | master Flash-variable field | 800F with a four-byte app length at the start of every sampled app |
801 | developer/signing key | 0104, the TI-83+/84+ freeware/shareware app key |
802 | program revision | one-byte revision, usually 1 |
803 | build number | one-byte build number, usually 1; MirageOS uses 2 |
804 | app name | up to 8 bytes; examples include Axe, MirageOS, USBDRV8X, and zStart |
808 | page count | one byte; matches the decoded page count for Axe and CtlgHelp’s two-page apps |
809 | disable TI splash screen | usually zero-length when present; zStart uses a 15-byte app-owned payload |
80C | lowest basecode | usb8x uses 02 1E, decoded as basecode 2.30 |
032 | date stamp | six bytes; bytes 1-4 decode as seconds since 1997-01-01 |
020 | date-stamp signature / unchecked payload | usually 64 bytes; Axe stores executable helper bytes here |
807 | final field | terminates the parsed header; the 807F length bytes are ignored |
The app header is not a fixed 128-byte struct. The 807 final field terminates it. The
common 80 7F 00 00 00 00 form uses size nibble F with a four-byte zero, but WikiTI
documents that length as ignored; the shorter 80 70 form is valid. The app body begins
after the final field and any app-controlled padding. Bytes before the conventional
4080 entry point are not loader magic; they are field payload or padding, and an app can
choose payload bytes that also decode as Z80. [standard]
External sample check (not ROM evidence): the local Axe Parser Axe.8xk sample decodes to
a base page whose 020D date-stamp-signature field starts at 4027 and has a 64-byte
payload. Part of that payload is a Z80 helper at 4037:
ti-kid identified this Axe header case and published an annotated decode in
Hatchet-Compiler;
the local decode below uses that lead and verifies it against the extracted Axe.8xk bytes.
4037 POP AF
4038 POP BC
4039 POP DE
403A POP HL
403B PUSH HL
403C PUSH DE
403D PUSH BC
403E PUSH AF
; ...
4056 LD A,0C9h
4058 CPIR
405A PUSH HL
405B IN A,(6)
405D DEC A
405E LD HL,4065h
4061 RST 20h
4062 JP 8478h
4065 OUT (6),A
4067 RET
RST 20h is _Mov9ToOP1, so the helper copies the thunk at 4065 into OP1
(0x8478) and jumps to OP1. That makes 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 sample | pages field / decoded pages | final field end | entry bytes at 4080 | header-area note |
|---|---|---|---|---|
| Axe | 2 / 2 | 4070 | 00 18 09 C3 97 40 C3 48 | 020 payload contains the 4037 helper; then padding |
| MirageOS | 1 / 1 | 4070 | C3 D3 65 C3 D9 47 C3 D6 | padding to 4080 |
| Omnicalc | 1 / 1 | 4070 | C3 8C 40 C3 E5 79 C3 70 | padding to 4080 |
| CalcSys | 1 / 1 | 4070 | C3 89 40 21 AA 98 CB DE | padding to 4080 |
| Symbolic | 1 / 1 | 4070 | 18 2E 3A 4A 42 4A 4D 4A | padding to 4080 |
| BatLib | 1 / 1 | 4070 | C3 25 61 C3 6E 43 C3 DE | padding to 4080 |
| BatLib-modified Celtic 3 / Grammer / Omnicalc | 1 / 1 | 4070 | app-specific jump/vector bytes | same boundary; nonzero 807F size bytes are ignored |
| zStart 1.3.013 / zStart83 | 1 / 1 | 4080 | 18 11 83 C3 ... | 809D0F carries a 15-byte Z80 helper at 406B |
| CtlgHelp / zChem from zStart | 2 / 2 or 1 / 1 | 4070 | app-specific bytes | padding to 4080 |
| usb8x | 1 / 1 | 4029 | 00 00 00 00 00 00 00 96 | mostly zero padding, plus JP 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 loopsapp_find_next_page (5FB1)+ a header-match step until done, returning the app’s start page and a found/not-found flag viaRST 28(bcall) into RAM flash helpers.5EE3 CALL 727D ; 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 scanningapp_find_next_page(3D:5FB1) —appSearchPage (0x82A3) -= 1; stops at page 7 (low boundary of the app region); bjumpsappSearchPage:0x4000to inspect the header.flash_set_sector_cnt(3D:727D→ helper726E) — initializes0x82A3to 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_5de7keeps two counts in BC (apps before/after) and tracks the current name in OP3._FindAppNumPagesis 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]
| Addr | String |
|---|---|
01:4076 | Defragmenting... |
01:4098 | Arc Vars Cleared |
01:40A9 | Apps Cleared |
01:40B8 | Arc Vars & Apps Cleared |
01:4109 | Resetting All... |
01:4126+412E | Garbage + Collecting... |
01:4234 | Resetting... |
01:7425..746E | menu titles: RESET MEMORY, RESET DEFAULTS, RESET ARC VARS, RESET ARC APPS, RESET ARC BOTH, RESET RAM |
01:747E | the long “Resetting ALL / RAM / Vars / Apps / Both …” warning help text |
2.2 The reset dispatcher (mem_reset_dispatch @ 35:7180) [confirmed]
Dispatch is on the selected reset item held in keyExtend (0x8446):
keyExtend | action | message shown |
|---|---|---|
| 1 | reset archived vars | Arc Vars Cleared (path 720B) |
| 2 | reset archived apps | Apps Cleared (path 7267) |
| 3 | reset both arc vars+apps | Arc Vars & Apps Cleared (path 7275) |
| 4 | reset all (RAM+archive) | Resetting All... (path 71F0) |
| else (0) | RAM reset (“RAM Cleared”) | wipe + re-init (path 719F) |
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:
- System RAM
0x8000–0x9BC3(~7 KiB: OS scratch, the Context block, system buffers). - 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
| bcall | addr | bit set | flag (inc) |
|---|---|---|---|
_SetFuncM | 36:7D11 | bit 4 (|0x10) | grfFuncM (Function) |
_SetPolM | 36:7D2C | bit 5 (|0x20) | grfPolarM (Polar) |
_SetParM | 36:7D39 | bit 6 (|0x40) | grfParamM (Parametric) |
_SetSeqM | 36:7D1F | bit 7 (|0x80) | grfRecurM (Sequence/Recursion) |
Each setter first calls a small predicate (36:0013/0254/0259/025E) and only re-sets
the mode if the parity/condition flag (F bit6) requires it, avoiding needless redraws.
Other grfModeFlags bits (from inc, not in the setters above): bit3 grfPolar
(rect↔polar coordinate readout). Related graph bytes: grfDBFlags (IY+0x04) bit0
grfDot (line/dot), bit1 grfSimul (sequential/simultaneous), bit4 grfNoCoord,
bit5 grfNoAxis; seqFlags (IY+0x0F).
3.3 Numeric format: Normal/Sci/Eng, Float/Fix, base — fmtFlags (IY+0x0A) [confirmed (bits from inc)]
fmtFlags byte at 0x89FA:
| bit | name | meaning |
|---|---|---|
| 0 | fmtExponent | 1 = show exponent (Sci/Eng), 0 = Normal |
| 1 | fmtEng | 1 = Engineering, 0 = Scientific (when exponent on) |
| 2-4 | fmtBaseMask (fmtHex/fmtOct/fmtBin) | integer base (Dec/Hex/Oct/Bin) |
| 5 | fmtReal | real display mode |
| 6 | fmtRect | rectangular complex display (a+bi) |
| 7 | fmtPolar | polar complex display (re^θi) |
So Normal/Sci/Eng = (bit0, bit1): Normal = 00, Sci = 01, Eng = 11.
fmtOverride (IY+0x0B, 0x89FB) is a working copy used during conversions.
Float vs Fix N is not in fmtFlags — it is the separate byte fmtDigits =
0x97B0: value 0x00–0x09 = Fix-N decimal places, 0xFF = Float.
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)
| Page | Funcs | Role | Representative routines |
|---|---|---|---|
00 | 928 | Kernel — mapped at 0000; RST vectors, bcall dispatcher, FP core, VAT, memory, integer math | _JErrorNo, _LdHLind, _DivHLBy10, _FindSym, _FPAdd, _InsertMem |
01 | 84 | Text display / homescreen | _PutMap, _PutC, _PutS, _DispHL, _NewLine, _ClrLCDFull |
02 | 271 | Float transcendentals & advanced math | _SqRoot, _LnX, _RnFx, _RndGuard |
03 | 23 | Edit-buffer / small font | _CloseEditBufNoR, _Load_SFont, _SFont_Len |
04 | 66 | Graph drawing (pixel/line) | _DarkLine, _ILine, _IPoint, _DarkPnt |
05 | 118 | TABLE editor + Graph-Table split-screen | table_editor_main, table_recompute, table_paint_grid |
06 | 49 | Key input & edit/cursor | _GetKey, _CursorOn, _CursorOff, _PutTokString (note _GetCSC’s body is on page 00) |
07 | 44 | Archive / list & matrix ops; error messages; large-font glyph table @ 07:45FF (7-byte stride) read by put_glyph_large (07:4588) | _Arc_Unarc, _CleanAll, _RedimMat, _IncLstSize, put_glyph_large |
33 | 70 | Graph coordinate math | _SetXXOP1, _UCLineS (window↔pixel transforms) |
36 | 24 | Mode setters (Func/Param/Polar/Seq) | _SetFuncM, _SetParM, _SetPolM, _SetSeqM |
37 | 23 | Graph coord convert | _XftoI, _YftoI |
38 | 277 | TI-BASIC parser / evaluator | _ParseInp, _Find_Parse_Formula, parse_init |
39 | 153 | Equation pretty-printer (2D MathPrint layout) + menus | eqdisp_render_entry, eqdisp_emit_glyph, _DispMenuTitle |
3A | 85 | Statistics (1/2-var, regressions) + TVM finance | _OneVar, reg_gauss_solve, tvm_solve_iterate |
34 | 16 | Crystal timers / clock, token scan | _CrystalTimerA, timer_scan_tbl |
35 | 6 | Memory-reset engine, factorial | mem_reset_dispatch, ram_reset_wipe, op1_factorial |
3B | 39 | bcall jump table + mem utils | (table data) _MemClear, _MemSet, _DrawCirc2 |
3C | 72 | Link / variable transfer | _SendAByte, _RecAByteIO, _SendVarCmd, _Rec1stByte |
3D | 61 | App management & Flash | _FindApp, _FindAppUp, _FindAppDn, _FlashToRam |
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):
| Page | Verified contents |
|---|---|
08–32 | Blank/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–39 | More OS code (graph/mode/menu/timers); fill 0.2–17% 0xFF. |
3B | bcall jump table — starts 99 27 00 = entry 0 (_JErrorNo→ram:2799). |
3C | Link code + the OS version string — page starts with ASCII 32 2E 35 35 4D 50 = "2.55MP". |
3E | Certification 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. |
3F | Retail 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:
| Window | Port | Selector encoding | Normal TI-OS value |
|---|---|---|---|
4000-7FFF | 6 | bit 7 clear selects Flash page value & 0x3F; bit 7 set selects RAM page 0x80 | (value & 7) | banked Flash page |
8000-BFFF | 7 | bit 7 clear selects Flash page value & 0x3F; bit 7 set selects RAM page 0x80 | (value & 7) | 81 |
C000-FFFF | 5 | low 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 page | Use |
|---|---|
80 | Normal C000-FFFF RAM page. The boot/home trace restores this with OUT (5),0. WikiTI marks it execution-protected. |
81 | Normal 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. |
82 | Not 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. |
83 | Shared 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. |
84 | Not used by TI-OS under typical execution; WikiTI marks it execution-protected. |
85 | Not used by TI-OS under typical execution on full-RAM hardware. |
86 | Not used by TI-OS under typical execution; WikiTI marks it execution-protected. |
87 | Not 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 page | Idle trace writes | 2+3 ENTER trace writes | Interpretation |
|---|---|---|---|
80 | 256227 writes, all page addresses touched | 345702 writes, all page addresses touched | Normal high RAM page selected by port 5; contains stack/system/user RAM activity in the C000-FFFF window. |
81 | 62947 writes, all page addresses touched | 72638 writes, all page addresses touched | Normal 8000-BFFF RAM page; contains the documented OS variables, flags, OP registers, heap, VAT window, and working buffers. |
82 | no writes observed | no writes observed | Port 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. |
83 | 1882 writes to 43D9-44BD and 5A7E-5DF2 | 3467 writes to 4373-4390, 43D9-44BD, 577E-5790, and 5A7E-5DF2 | Shared OS scratch/state page. See the range table below. |
84 | no writes observed | no writes observed | No typical-use OS storage confirmed in these traces. |
85 | no writes observed | no writes observed | No typical-use OS storage confirmed in these traces. |
86 | no writes observed | no writes observed | No typical-use OS storage confirmed in these traces. |
87 | no writes observed | no writes observed | No 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/path | How to hit it | Evidence |
|---|---|---|
80 high RAM | Run any cold-boot, home, expression, or graph trace. | Port 5 = 00 is the normal restore value; every current trace writes all page-80 addresses. [confirmed] |
81 normal bank-B RAM | Run any cold-boot, home, expression, or graph trace. | Port 7 = 81 is the normal restore value; every current trace writes all page-81 addresses. [confirmed] |
83 display capture | Run boot-idle.macro or graph-y1-x2.macro. | Ghidra shows _SaveDisp (39:5DD8) calls lcd_read_block (ram:1890) at the 39:5E03 call site; coverage hits both, and writes 5A7E-5D7D. [confirmed] |
83 homescreen previous-entry history | Run home-2plus3.macro. | The trace adds 577E-5790, advances lastEntryPTR from 577E to 5791, and sets numLastEntries to 01. [confirmed] |
83 expression scratch copy | Run home-2plus3.macro. | The trace adds 4373-4390 through flash_copy_block at ram:1868/ram:187C. [confirmed] |
83 split-screen/table copy | Enter 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 initialization | Enter an edit-buffer workflow that reaches editbuf_init_buf. | Ghidra shows editbuf_init_buf at 03:6BC4 calls flash_copy_block at 03:6BCD; this path is not hit by the current macros. [confirmed] |
83 app-menu state restore | Open an app/menu workflow that reaches mnu_restore_app_state. | Ghidra shows mnu_restore_app_state at 39:6D96 calls flash_copy_block at 39:6DA0; this path is not hit by the current macros. [confirmed] |
84-87 independent pages | Use a forced RAM-page probe or a ROM path that passes pair index 2 or 3 to the computed bank-pair helper. | The ROM can compute these selectors, but raw immediate selector scans and current traces do not show a normal OS path selecting or writing them. [hypothesis] |
The computed bank-pair helpers use this selector formula:
LD A,B
SLA A
OUT (5),A ; pair index 0/1/2/3 -> pages 80/82/84/86 in bank C
INC A
OR 0x80
OUT (7),A ; pair index 0/1/2/3 -> pages 81/83/85/87 in bank B
Decoded callers set B = 1, selecting pages 82/83; that explains the observed
port 5 = 02, port 7 = 83 sequence. 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:
| Range | Use | Evidence |
|---|---|---|
4373-4390 | Expression-path page-83 scratch copy | Added 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-44BD | Boot/home page-83 scratch copy | Present 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-5A7D | Homescreen previous-entry history | Page 33 references 577E, the 5A7E upper bound, lastEntryPTR (0x8DA7), and numLastEntries (0x8E29). The 2+3 ENTER trace writes 577E-5790, advances lastEntryPTR to 5791, and sets numLastEntries to 01. [confirmed] |
5A7E-5DF2 | LCD/home display capture area | Present in the idle trace. The _SaveDisp LCD capture (ram:1890) fills the first 0x300 bytes, 5A7E-5D7D (the 96×64 framebuffer); the 5D7E-5DF2 tail is additional page-83 writes in the same scenario. Ghidra decompiles ram:1890 as an LCD-read helper that maps page 83 through port 6 and stores bytes read from LCD port 11. [confirmed] |
4000-4080 | App base-page staging before app execution | WikiTI public note; the two traces on this page do not launch an app. [standard, not traced here] |
4100-433A | USB communication buffers | WikiTI 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:
| Scenario | lastEntryPTR (0x8DA7) | numLastEntries (0x8E29) |
|---|---|---|
| Idle home screen | 577E | 00 |
After 2+3 ENTER | 5791 | 01 |
Those values come from end-of-trace RAM reconstruction. The added page-83 range
577E-5790 is exactly the bytes between the old and new lastEntryPTR values.
[confirmed]
Restoring after page 83
Restore the selector for every window you changed. For code entered from normal TI-OS
state that temporarily maps page 83 into bank B (8000-BFFF) and page 82 into
bank C (C000-FFFF), restore the two RAM windows this way:
LD A,0x81
OUT (7),A ; 8000-BFFF back to RAM page 81
XOR A
OUT (5),A ; C000-FFFF back to RAM page 80
For code that maps page 83 into bank A (4000-7FFF), preserve and restore port 6:
IN A,(6)
PUSH AF
LD A,0x83
OUT (6),A ; map RAM page 83 at 4000-7FFF
; use 4000-7FFF here
POP AF
OUT (6),A ; restore previous Flash/RAM page selector
Keep the nonstandard mapping inside a short critical section. The OS helper preserves
interrupt state around the temporary RAM-page mapping so the interrupt handler does not
run with bank A or bank B pointing at page 83.
For code that may be called with nonstandard paging, preserve and restore the selectors for all touched windows:
IN A,(6)
PUSH AF
IN A,(7)
PUSH AF
IN A,(5)
PUSH AF
LD A,0x83
OUT (7),A ; map RAM page 83 at 8000-BFFF
; use 8000-BFFF here
POP AF
OUT (5),A
POP AF
OUT (7),A
POP AF
OUT (6),A
The OS’s own paged byte-store helper at 37:44AE uses the normal restore pattern:
37:44D0 OUT (5),A ; A = page index << 1, trace case A = 0x02 (→ RAM page 82)
37:44D2 INC A ; A = 03
37:44D3 OR 0x80 ; A = 0x83
37:44D5 OUT (7),A ; trace case: 0x83
37:44D7 LD A,B
37:44D8 LD (DE),A ; byte store while RAM page 83 is visible
37:44D9 LD A,0x81
37:44DB OUT (7),A
37:44DD XOR A
37:44DE OUT (5),A
The dynamic trace resolves the same sequence at instruction indices 712241-712250,
including the final port 7 = 81 and port 5 = 00 writes. [confirmed]
05 — Variables & the VAT (Variable Allocation Table)
Deep dive: Variables, Archive & Unarchive — Store/Recall, the byte-verified
_FindSymwalk, and archive/unarchive.
Every named object the user creates — reals, lists, matrices, strings, programs, pictures, appvars, groups — is catalogued in the VAT, a table in RAM that grows downward from a fixed top. The VAT stores metadata + where the data lives; the data itself sits elsewhere in RAM (or in archived flash).
Object types — TIVarType enum [confirmed — generated tools/ty_vartype.txt, per ti83plus.inc]
| Val | Name | Val | Name |
|---|---|---|---|
| 0x00 | RealObj | 0x0C | CplxObj |
| 0x01 | ListObj | 0x0D | CListObj |
| 0x02 | MatObj | 0x0E | UndefObj |
| 0x03 | EquObj | 0x0F | WindowObj |
| 0x04 | StrngObj | 0x10 | ZStoObj |
| 0x05 | ProgObj | 0x11 | TblRngObj |
| 0x06 | ProtProgObj | 0x12 | LCDObj |
| 0x07 | PictObj | 0x13 | BackupObj |
| 0x08 | GDBObj | 0x14 | AppObj |
| 0x09 | UnknownObj | 0x15 | AppVarObj |
| 0x0A | UnknownEquObj | 0x16 | TempProgObj |
| 0x0B | NewEquObj | 0x17 | GroupObj |
The active object’s type byte is held in varType (0x85D0); the current var being processed in curType (0x8450). Both are typed TIVarType in the DB.
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:
| Routine | Addr | Role |
|---|---|---|
_FindSym | 00:0E65 | find the VAT entry named by OP1; returns ptr/page (also the RST 10h fast path: vector 00:0010 → JP 0E65) |
_ChkFindSym | 00:0E60 | type-classify OP1 (via the helper at ram:2042, which calls _CkOP1Real 00:1942 then checks the findable var classes) then _FindSym |
_CreateReal | 00:10B8 | make a RealObj named by OP1 |
_CreateReal/_CreateCplx/_CreateRList/_CreateCList/_CreateRMat/_CreateStrng/_CreateProg/_CreateAppVar/… | 00:10B0–00:1153 | one exported creator bcall per creatable variable class — ~13 _Create* routines covering the creatable classes, not one per TIVarType (no _CreateList/_CreateMat/_CreateStr); some object types are made only by internal routines with no public _Create* bcall (e.g. the GroupObj creator at 00:1157, called from 39:73AF) |
_DelVar/_DelVarArc | 00:1308/00:12D9 | delete (and handle archived copies) |
_InsertMem/_DelMem | 00:0F81/00:1368 | public low-level grow/shrink of a RAM region (the create path instead uses the internal gap routine at ram:0F0C) |
_CreateReal (recovered): sets the type byte and a fixed size of 9, then jumps to the common create core at 00:1011. That core stores the type, type-checks the object (chk_type_not_str at ram:2045), handles the complex-list special case (OP1.exp == 0x5D), applies the 6-character name limit (00:1023 CP 0x7; 00:1025 JP NC,00:2700 → LD A,0x88, E_Syntax), and carves the gap via the internal routine at ram:0F0C (00:1034). Aggregate creators (lists/matrices) instead enter through the size prelude var_alloc (00:1005), which computes count×element-size + the 2-byte header and raises E_Memory on overflow (JP C,00:2721 at 00:1008 → LD A,0x8E) before falling into the same 00:1011 core.
Variable data formats — rendered as C [confirmed 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:
| Type | Val | dataAddr → | Size (bytes) |
|---|---|---|---|
RealObj | 0 | one TIFloat | 9 |
CplxObj | 0x0C | one TIComplex (re, im) | 18 |
ListObj / CListObj | 1 / 0x0D | count word + count×TIFloat/TIComplex | 2 + 9·n / 2 + 18·n |
MatObj | 2 | rows,cols bytes (dim0=rows) + TIFloat[], column-major (index math in Matrices & Lists) | 2 + 9·r·c |
EquObj | 3 | size word + tokenized formula — system var, carries a selection/style byte, auto-evaluated (Graphing, Table) | 2 + size |
StrngObj | 4 | size word + tokenized text — inert (see Strings) | 2 + size |
ProgObj / ProtProgObj | 5 / 6 | size word + tokenized program (6 = edit-locked) | 2 + size |
AppVarObj | 0x15 | size word + raw bytes (any binary, not tokens) | 2 + size |
PictObj | 7 | a graph back-buffer image (plotSScreen snapshot) | 756-byte payload + 2-byte size word = 758 (_CreatePict passes payload size 0x02F4) |
GDBObj | 8 | graph database: mode byte + window vars + selected equations + styles | varies |
GroupObj | 0x17 | an archived bundle of other vars (lives in Flash) | varies |
WindowObj/ZStoObj (0x0F/0x10) hold the graph Window settings, TblRngObj (0x11) the table range, BackupObj (0x13) a full RAM image — all system, fixed-shape blobs.
Aggregate creators size their data region (= count × element-size + 2-byte header) in the var_alloc prelude (ram:1005), then fall into the common create core (ram:1011), which carves the gap via the internal routine at ram:0F0C (the create path’s own block-move, not the public _InsertMem; see 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:0E65 → findsym_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 0x8479–0x847B; 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-2 | the name bytes (matched against OP1’s 0x8479–0x847B) |
N+1 | data page (B; 0 ⇒ data in RAM) |
N+2 / N+3 | data address — high byte, then low byte |
N+6 | type — 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 (tVarLst0x5D),[A]-matrices (tVarMat0x5C), system vars, and the token-named strings (tVarStrng0xAA+ id) and equations (tVarEqu0x5E+ id) — carry a fixed name token, matched by the 1–3 byte compare above. - Length-prefixed names — programs, appvars, groups — store the name bytes with a length byte at the higher address; the scanner at
07:55D1reads the length byte, then compares the name bytes that precede it (downward, toward lower addresses — the same high-address-first ordering as the rest of the entry).
Strings (Str1–Str0) — a distinct object type [confirmed]
String variables are StrngObj (type 4) — not equation variables (EquObj = 3), although both hold tokenized byte streams. The ten strings Str1…Str0 are named by a 2-byte token: lead tVarStrng (0xAA) then tStr1…tStr0 (0x00…0x09), so Str1 = AA 00 … Str0 = AA 09.
Storage. _CreateStrng (id 0x4327, 00:1123) decompiles to create_var_entry(StrngObj) followed by writing a 2-byte word size into the data; the data area is then [word size][size tokenized bytes] — the same [size][bytes] shape programs and appvars use (above). The bytes are TI-BASIC tokens, not raw ASCII: a string stores exactly the token stream the editor renders, so "sin(A)" keeps the sin( token, the A token, and ) — which is why a string can hold any displayable token, commands included.
String vs. equation variable. Both hold tokenized byte streams, so the two are worth separating. EquObj vars (Y1–Y0, parametric, polar, sequence) are system variables that carry a selection/style flags byte and are auto-evaluated by the grapher, table, and solver (see Graphing, Table & Y= Variables). A StrngObj is an inert user variable — no selection/style, never evaluated on its own; it is bytes the string commands manipulate.
Bridges between the two. Tokens convert a string’s text to/from executable form:
expr(parses a string’s token bytes as an expression and evaluates it → a value (string → number/list/…).String►Equ(/Equ►String((2-byte tokensBB 56/BB 55—t2ByteTok0xBBthentStrngToEqu0x56/tEquToStrng0x55) copy token bytes between aStrand aY=/equation variable (string ↔ equation).sub(,length((_StrLength, id0x4C3F→36:7F91), andinString(operate on the token bytes;_StrCopy(0x44E3→00:2810) is the byte mover. The"string-literal delimiter in source is its own token,tString(0x2A).
FindSym scan and VAT entry layout
The _FindSym scan loop and per-class VAT entry layout are byte-verified in Variables, Archive & Unarchive (findsym_scan@07:565F; tSymPtr1/tSymPtr2 and archived-var resolution covered there).
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]
OP1–OP6 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:
| Routine | Addr | Shortcut | Effect |
|---|---|---|---|
_FPAdd | ram:229E | RST 30h | OP1 ← OP1 + OP2 |
_OP1ToOP2 | ram:1A2F | RST 08h | copy OP1 → OP2 (11 bytes, via copy_op11 ram:1a8e) |
_Mov9ToOP1 | ram:1B01 | RST 20h | load 9 bytes at HL → OP1 (a constant/var) |
_CkOP1FP0 / _CkOP2FP0 | ram:1DE9 / ram:1DEE | — | test OP1/OP2 == 0 (sets Z) |
_CkOP1Real | ram:1942 | — | type-check OP1 is real |
Alignment, then the worked example — _FPAdd
To combine $x=(-1)^{s_x} m_x\times 10^{e_x}$ and $y=(-1)^{s_y} m_y\times 10^{e_y}$, the engine first aligns to the larger exponent. With $e_x \ge e_y$ it shifts $m_y$ right by
$$\Delta = e_x - e_y \quad(\text{digit shifts})$$
one nibble per fp_shift_right_digit call; if $\Delta > 15$ the smaller operand falls entirely past the 14 mantissa digits plus the 2 guard digits and is dropped. It then adds the aligned mantissas when the signs match ($s_x = s_y$) and subtracts when they differ ($s_x \ne s_y$), fixing the result’s sign afterward — the essence of sign-magnitude arithmetic:
\begin{algorithm}
\caption{\texttt{\_FPAdd}: $OP1 \gets OP1 + OP2$ (sign-magnitude BCD)}
\begin{algorithmic}
\IF{$OP2 = 0$}
\RETURN $OP1$
\ENDIF
\IF{$OP1 = 0$}
\STATE $OP1 \gets OP2$ \COMMENT{incl. extended bytes}
\RETURN $OP1$
\ENDIF
\STATE $\Delta \gets \mathrm{exp}(OP1) - \mathrm{exp}(OP2)$ \COMMENT{\texttt{fp\_exp\_diff}}
\STATE shift the smaller mantissa right by $|\Delta|$ digits to align \COMMENT{\texttt{fp\_shift\_right\_digit}}
\IF{$|\Delta| > 15$}
\RETURN larger operand \COMMENT{other is negligible}
\ENDIF
\IF{$\mathrm{sign}(OP1) = \mathrm{sign}(OP2)$}
\STATE $\mathrm{mantissa} \gets$ BCD-add
\ELSE
\STATE $\mathrm{mantissa} \gets$ BCD-subtract; 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+3run (home-2plus3.macro) enters_FPAddand — signs equal — falls through the sign test tofp_add_mantissa(ram:1cb9), while the5−2run (fpsub.macro) negatesOP2and takes the opposite-sign branch intofp_sub_mantissa(ram:1d37).fp_sub_mantissahas 0 hits in the add trace and the add path 0 hits in the subtract trace, so the pseudocode’s sign dispatch is confirmed both ways.
The FP helper cluster [confirmed]
These five page-0 primitives are shared by add/sub/mult/div and the transcendentals. All were decompiled and disassembled in this ROM; the fp_* names below are the project’s labels (in tools/names.txt). They operate on the OP-register guard region (OP1EXT/OP2EXT are 2 bytes each, at 0x8481–0x8482/0x848C–0x848D — fp_clear_guard zeroes all four) and the 7-byte mantissas of OP1 / OP2 (OP1M 0x847A / OP2M 0x8485, two bytes past the type/exponent bytes at 0x8478/0x8483).
| Helper | Addr | Role [confirmed] |
|---|---|---|
fp_shift_right_digit | ram:1bea | Mantissa shift-right by one BCD digit (one nibble). Cascades nibbles down 8 bytes (b[i] = b[i]>>4 | b[i-1]<<4) and returns the digit shifted out. Called per step to align the smaller operand. |
fp_exp_diff | ram:1fbf | Exponent difference OP1.exp − OP2.exp (signed). Drives how many fp_shift_right_digit steps are needed for alignment. |
fp_add_mantissa | ram:1cb9 | BCD add of the two mantissa+guard runs. Sets HL=0x848C (OP2 guard), DE=0x8481 (OP1 guard) and runs the shared BCD add/DAA-style adjust loop (bcd_add_pair). Used for same-sign add. |
fp_sub_mantissa | ram:1d37 | BCD subtract (OP1 − OP2) of mantissa+guard with borrow, via repeated DAA-style BCD adjust across all 7 mantissa bytes plus the guard byte. Used for opposite-sign add. (ram:1d2f, fp_sub_mantissa_fwd, is the same subtract entered with the operand pointers swapped.) |
fp_clear_guard | ram:2627 | Zero the extended guard bytes (OP1EXT/OP2EXT). |
ram:1d2f and ram:1d37 are two entry points into the same BCD-subtract body — 1d2f loads HL=0x8481 (OP1 guard), DE=0x848C (OP2 guard) and computes OP2 − OP1 into OP2 (LD A,(DE); SUB (HL)), while 1d37 enters with the pointers swapped for the reverse OP1 − OP2, before joining the common loop — so the caller picks the subtraction direction by choosing the entry. This is what lets _FPAdd produce a non-negative magnitude and then fix the sign.
Multiply/divide/transcendentals (on page 0x02) reuse the same align/normalize primitives.
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:
| Routine | Addr | Role |
|---|---|---|
_FPSub | ram:2297 | OP1 = OP1 − OP2 |
_FPMult | ram:238B | OP1 = OP1 × OP2 |
_FPRecip | ram:253D | OP1 = 1 / OP1 |
_FPDiv | ram:2541 | OP1 = OP1 / OP2 |
_LnX | 02:6EFD | natural log |
_EToX | 02:705C | eˣ |
_SinCosRad | 02:733E | sin/cos (radians) |
See Calculation Engine for the ×/÷/^/root algorithms and number formatting.
Transcendental method [confirmed]
The ln/e^x/sin-cos evaluators 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:1ca9 — 1cb9 fires 0× in the ln(2) loop, 1ca9 0× in the e¹ loop.)
Logarithm. With the exponent already split off so the mantissa is $x\in[1,10)$, the loop (02:6F80–6FEE) drives $x$ up toward $10$ by repeatedly scaling by the largest table factor that doesn’t overshoot; the number of scalings at each position is the corresponding digit of the answer, and the running sum of the table entries is the logarithm:
\begin{algorithm}
\caption{Logarithm by pseudo-division (table $c_k=\log_{10}(1+10^{-k})$ at \texttt{02:7181})}
\begin{algorithmic}
\REQUIRE reduced mantissa $x \in [1,10)$, accumulator $L \gets 0$
\FOR{$k = 0$ \TO $15$}
\WHILE{$x \cdot (1+10^{-k}) \le 10$}
\STATE $x \gets x + (x \gg k\text{ digits})$ \COMMENT{$\times(1+10^{-k})$ is a BCD shift-add}
\STATE $L \gets L + c_k$ \COMMENT{add count = the $k$-th digit of the answer}
\ENDWHILE
\ENDFOR
\RETURN $\log_{10}x = 1 - L$ \COMMENT{$x$ driven up to $10$; then $\ln x = \log_{10}x \cdot \ln 10$}
\end{algorithmic}
\end{algorithm}
The 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) stepsA=00…07then08…0F(the coarse/fine split at the6FAD AND 0x8/6FD3 BIT 4tests), walking successive02:7181rows with a per-step shift-add, then fetches $\ln 10$ viaLD A,6; CALL ram:2362and multiplies.e^{1}(exp1.macro) drives_EToX, which consumes the same table in reverse (the inner step isfp_sub_mantissa1d37, the accumulator addfp_add_mantissa1cb9), selector sweeping00…0Funder the710A CP 0x0Fbound. On-screen results:.6931471806and2.718281828.
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:6F45–02:6F50 (a value formed with _FPAdd (RST 30h) / _FPSub / _FPDiv ram:2541), the pseudo-division loop described above (02:6F8C–02: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:6FAB–02:6FAF, coarse digits) and the second when it reaches bit 4 (02:6FD2–02: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:7069–02:70B6), handles sign/reciprocal cases (02:70B9–02: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:
- Mode/select flags.
0x8499holds the trig-op selector —0x01(sin),0x02(cos),0x04(tan) — ORed with0x80when(IY+0)bit 2 is clear (BIT 2,(IY+0); JR NZ,+2; OR 0x80)._SinCosRaditself enters withA=0x81, so it stores0x81regardless.fp_clear_guardand_ZeroOP3initialize the work area. - Exponent gate.
LD A,(0x8479); SUB 0x80; JP C,02:73D4; CP 0x0C; JP NC— tiny arguments (negative exponent) take a fast path at02:73D4, and arguments with decimal exponent ≥ 12 are rejected to the slow/error path (_JError 0x84for out-of-range), because reduction can no longer be done accurately. - Reduce the angle. It reduces against the stored period constants and takes the fractional part to find the quadrant. The reduction constants are the page-0x02 BCD block:
02:7D81— the 2π full-turn modulus (mantissa62 83 18 53 07 17 96=6.2831853…), copied to the OP3 work reg viaLD HL,02:7D81; CALL ram:1AE2(ram:1AE2/copy7_from_8490copies 7 mantissa bytes to0x8490).02:7D8E,02:7D95,02:7D96— companion constants used in the quadrant-fixup / remainder comparisons (CALL ram:1D7Bmagnitude compare at02:73B1/02:7447). The quadrant (0–3) is accumulated inB/bStack_1(bits 0/3/6) and decides sin-vs-cos and the result sign (theXOR 0x1 / OR 0x8 / XOR 0x8flag juggling at02:7424–02:7464).
- Per-digit evaluation. After reduction (
02:7475onward, falling through02: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:731Dreads the signed table at02:7201,02:74EA: CALL 02:7312reads the signed table at02:7281— and the selector advances underBIT 3,B/BIT 3,Cin the tail (02:74DD–02:74E0,02:75C6–02:75C8), so the walk covers eight selector rows (0..7), each carrying two 8-byte sign/phase variants. Per-row decoding of02:7201/02:7281is 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, sinceexp(1)=0is in[0,12)), the reduction multiply by the02:7D81constant (7372 LD HL,02:7D81; CALL ram:1AE2), and the table-stepping loader02:731Dreturning successive rows of02: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 (OP1–OP6 @ 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.
| Reg | Addr | Role in a calc |
|---|---|---|
OP1 | 0x8478 | primary accumulator / result. Unary ops take arg here, return here. |
OP2 | 0x8483 | second operand for binary ops (OP1 ∘ OP2 → OP1). |
OP3–OP6 | 0x848E… | scratch; sign/exponent staging, complex pairs. |
| guard | 0x8481/8482 (OP1EXT), 0x848C/848D (OP2EXT) | extended guard digits, zeroed by fp_clear_guard at the top of nearly every op. |
FPS | 0x9824 | software FP stack for spilling OP registers during nested evaluation. |
A TIFloat is type(+0) exp(+1) mantissa(+2..+8); type bit7 = sign, bits set 0x0C = complex.
exp is base-10 biased by 0x80. Sign is sign-magnitude, so negation is a single XOR 0x80.
FP-stack discipline during nested expressions [confirmed]
Binary/transcendental routines that need to preserve an operand spill it to FPS:
_PushRealO1(=RST 18h,ram:155C),_PushReal/_PushRealOn,_PushOP1(ram:1599)._PopRealO1…_PopRealO6(ram:150F…14F6),_PopReal(ram:1512)._AllocFPS/_DeallocFPS(ram:1534/1526) grow/shrink the stack frame.
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.
| Op | Routine | Addr | Notes |
|---|---|---|---|
+ | _FPAdd | ram:229E (= RST 30h) | sign-magnitude BCD add; see 06-floating-point.md. |
− | _FPSub | ram:2297 | flips OP2.type bit7 then falls into the add path. |
× | _FPMult | ram:238B | ram:250F adds exponents (→ _ErrOverflow on carry past 0x7F), then digit-by-digit BCD multiply accumulating into OP3. |
÷ | _FPDiv | ram:2541 | _CkOP2FP0 first → _JError(0x82) DIVIDE BY 0 if divisor 0; else restoring BCD long division. |
1/x | _FPRecip | ram:253D | sets OP1=1 then enters the divide loop (same body as _FPDiv). |
Convenience / derived ops:
_FPSquareram:238A=RST 08h(OP1→OP2) then_FPMult. [confirmed]_Cuberam:237D=_FPSquarethen_FPMult. [confirmed]_Times2ram:2282=OP1+OP1;_TimesPt5ram:2382loads the constant0.5(9-byte BCD @ram:2635) into OP2 then_FPMult. [confirmed]_InvSubram:227D=_InvOP1Sthen_FPAdd⇒OP2 − OP1(reversed subtract). [confirmed]- Negation:
_InvOP1Sram:24BD(XOR OP1.type with 0x80, guarding against −0),_InvOP2Sram:24CD,_InvOP1SCram:24BA(both)._CkOP1Posram:1E5DANDs OP1.type with 0x80. [confirmed]
Roots & integer parts [confirmed]
_SqRoot02:6E38:_ErrD_OP1NotPos(→ DOMAIN if negative/complex-real),fp_clear_guard,_ZeroOP3, then a digit-by-digit BCD square-root extraction loop (ram:1C9Ctrial-subtract +ram:1D4Acompare, halving the exponent up front). A classic long-hand sqrt, not Newton’s method._Int/_Intgrram:2621/2263: floor._Truncram:2279drops the fractional part (toward zero);_Intgrtruncates then subtracts 1 (_Minus1ram:2294) when the original was negative, giving true floor._Fracram:24E3: fractional part = x − trunc(x); shifts mantissa by the exponent and keeps the low digits._Round/_RndGuardram:2623/02:6A57: round to the active display-digit count;_Roundis a thincross_page_jumpwrapper (body banked off page 0).
3. Degree/radian & polar conversions [confirmed]
_DToRram:236B(deg→rad): multiply OP1 by $\pi/180$ (ram:235Dloads the constant) then normalize viaram:249E._RToDram:2374(rad→deg): multiply by $180/\pi$ (ram:2361)._PToR02:50BDpolar→rectangular; pairs with the complex trig below. These constants are the BCD floatsπ/180 = 1.745…e-2and180/π = 5.729…e1noted in 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:
- saves the current page (
IN A,(6)), - builds a
RET-to-page-0 trampoline on the stack (bcallreturn frame, page restored on exit), - reads a 3-byte
{lo, hi, page}descriptor (page masked with0x1F/0x3Ffor 83+/84+ via ports 2/0x21), OUT (6),Ato bank the target page in at4000, then jumps to it.
The ln/e^x sites that look similar are local calls, not cross-page dispatches. 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]
_LnX02:6EFD:_CkOP1Pos; non-positive real →_ErrDomain. For a positive real it calls the real-log core (_CLNpath, selectorC=2); the generic entry handles complex args._LogX02:6F16: same structure, base-10 selectorC=0, guards_ErrD_OP1_0/_ErrD_OP1NotPos._CLN02:6CCA/_CLog02:6CE7— complex log:_CAbs(magnitude) → real_LnX/_LogXfor the real part,_ATan2Rad(02:76D4) for the imaginary part (the argument/angle). Uses_PushRealO1/_PopRealO2to juggle the operand. This is whyln(-2)returns a complex result ina+bimode but raises_ErrNonReal(0x87) in real mode.
Exponentials [confirmed]
_EToX02:705C(e^x): loads thelog10(e)constant throughram:2362/02:7D1E, then falls through into the local_TenXbody._TenX02:7066(10^x): splits exponent into integer (digit shift) + fractional (16-slot table-driven evaluation through02:7181). Argument too large →_ErrOverflow.
Trig — sin/cos/tan [confirmed]
_SinCosRad02:733E,_Sin7342,_Cos7346,_Tan734A. Each loads a function selector byte into0x8499(1=sin,2=cos,4=tan;0x80bit set when the rad-special mode tested byBIT 2,(IY+0)is off;_SinCosRadforces0x81).- Range reduction: reads OP1 exponent; exponent ≥ 0x0C (|x| ≳ 10^12·) →
_ErrDomain(“argument out of range”). It then reduces the angle modulo a quarter-period using the BCD constant table near02:7D81and runs the same table-driven digit recurrence as ln/eˣ over the signed near-unity tables at02:7201and02:7281(one row per digit step, sign-variant picked by0x84A4bit 7) — the per-stepbcd_sub_op1_op2(ram:1D8A) /bcd_add_8496_8480(ram:1D26) are the shift-and-add BCD steps of that recurrence, not a fixed polynomial and not CORDIC for the forward trig. The per-row decoding of02:7201/02:7281is detailed in 06-floating-point.md.
Inverse trig [confirmed]
_ASinRad76DA,_ACosRad76C9,_ATanRad76CF,_ATan2Rad76D4, plus the degree-mode_ASin/_ACos/_ATan/_ATan2at76F1/76DF/76E9/7749._ASin/_ACoscall domain check02:79D3; |arg| > 1 →_ErrDomain.- All inverse trig funnel into the shared arctangent CORDIC engine at
02:774B(B=0x20seeds the octant/quadrant base written to0x84A4; the core is a base-10 trial-subtract digit recurrence, not a fixed 32-step loop), with asin/acos expressed via atan2 of (x, √(1−x²)).
Hyperbolics [confirmed]
_SinHCosH7626,_TanH762A,_CosH762E,_SinH7632;_ATanH/_ASinH/_ACosHat7909/7956/7964. Same0x8499selector mechanism; built from_EToX(sinh = (e^x−e^-x)/2, visible in the_EToX+_FPDivsequence near02:6D08).
Power operator ^ [confirmed]
The general a^b lives at 02:6D08+: it computes b·ln(a) then e^() and reconstructs
with _SinCosRad for the complex case — i.e. a^b = e^(b·Ln a), with _FPDiv/_FPMult
glue and _OP2ToOP6/_OP6ToOP1 shuffles. Integer/√ special cases short-circuit to
_FPMult/_SqRoot. 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).
_FormReal06:5ACF— real-number formatter. [confirmed]fp_clear_guard; zero →_OP1Set0; copies arg to OP5.- Reads the digit-count/mode flags from
(IY+0xc)and the byte at0x89FA(active fixed/decimal-places setting;(IX-1)local holds the effective format byte). - Exponent thresholds drive Normal↔Sci switchover: it compares
OP1.expagainst0x7D/0x7F(≈ the ±-exponent window) and renormalizes (ram:1BE7) to bring the value into the displayable mantissa range, bumping a digit counter. Negative sign decrements the leading column count (DEC (IX-3)).
_FormEReal06:5799— forces scientific/E notation by setting0,(IY+0xc)then calling_FormReal. [confirmed]_FormBase06:57C0— integer formatting in a base; requires_CkOP1Real(→ DATA TYPE / DOMAIN on non-real). [confirmed]_FormDCplx06:59D3— complexa+bi/r∠θformatting (calls_FormRealtwice). [standard]- Exponent ↔ ASCII helpers on page 0:
_ExpToHexram:1E4E,_OP1ExpToDecram:1E77,_DecO1Expram:1E6F(decrement exp),ram:1BCB(BCD-digit → value). [confirmed] - The formatted string is then drawn by
_DispOP1A(04:7844) / homescreen put-string routines (see 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:
| Raiser | Addr | A code | Message |
|---|---|---|---|
_ErrOverflow | ram:26E8 | 0x81 | OVERFLOW |
_ErrDivBy0 | ram:26EC | 0x82 | DIVIDE BY 0 |
_ErrSingularMat | ram:26F0 | 0x83 | SINGULAR MAT |
_ErrDomain | ram:26F4 | 0x84 | DOMAIN |
_ErrIncrement | ram:26F8 | 0x85 | INCREMENT |
_ErrNon_Real | ram:26FC | 0x87 | NONREAL ANS |
_ErrSyntax | ram:2700 | 0x88 | SYNTAX |
_ErrMode | ram:2704 | 0x9E | MODE |
_ErrDataType | ram:2708 | 0x89 | DATA TYPE |
_ErrArgument | ram:2711 | 0x8A | ARGUMENT |
_ErrDimMismatch/Dimension | ram:2715/2719 | 0x8B/0x8C | DIM MISMATCH / INVALID DIM |
_ErrUndefined/Memory | ram:271D/2721 | 0x8D/0x8E | UNDEFINED / MEMORY |
Domain pre-checks (page-0, set Z if OK else jump to _ErrDomain):
_ErrD_OP1NotPosram:2119—_CkOP1Pos; not >0 ⇒ DOMAIN (used by_SqRoot,_LogX)._ErrD_OP1Not_Rram:2120—_CkOP1Real; complex ⇒ DOMAIN._ErrD_OP1NotPosIntram:2125—_CkPosInt._ErrD_OP1_LE_0ram:212A,_ErrD_OP1_0ram:212D— zero/sign guards (e.g.ln(0)).
Where the calc engine raises what:
÷ 0,1/0:_FPDiv/_FPRecip→0x82DIVIDE BY 0.×/10^x/exponent overflow:ram:250Fexponent-add →0x81OVERFLOW.√(neg),ln/log(≤0),asin/acos(|x|>1),tan(π/2), |trig arg| ≳ 10^12:0x84DOMAIN.- Complex result requested in real mode:
0x87NONREAL ANS (the_CLN/complex paths).
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 2119–2131.
9. Worked flow: 2*sin(π/6)+ln(5) [hypothesis, from the above]
- Parser pushes
2(OP1), evaluatessin(π/6): loadsπ/6into OP1,_SinCosRad/_Sin(selector0x8499), table-driven digit recurrence →OP1=0.5. ×: the saved2is inOP2(or popped from FPS) →_FPMult→OP1=1.ln(5): spill1to FPS (_PushRealO1),OP1=5,_LnX(_CkOP1Pospasses) →1.6094….+: pop1toOP2(_PopRealO2),_FPAdd→OP1≈2.6094._FormRealrenders per MODE; result stored asAns.
Statistics
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̄, Σ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):
| Addr | Name (.inc) | User-facing var | Meaning |
|---|---|---|---|
8A3A | StatN | n | sample count (Σ of frequencies) |
8A43 | XMean | x̄ | mean of x |
8A4C | SumX | Σx | sum of x |
8A55 | SumXSqr | Σx² | sum of x² |
8A5E | StdX | Sx | sample std dev of x (÷ n−1) |
8A67 | StdPX | σx | population std dev of x (÷ n) |
8A70 | MinX | minX | minimum x |
8A79 | MaxX | maxX | maximum x |
8A82 | MinY | minY | minimum y (2‑Var) |
8A8B | MaxY | maxY | maximum y (2‑Var) |
8A94 | YMean | ȳ | mean of y |
8A9D | SumY | Σy | sum of y |
8AA6 | SumYSqr | Σy² | sum of y² |
8AAF | StdY | Sy | sample std dev of y |
8AB8 | StdPY | σy | population std dev of y |
8AC1 | SumXY | Σxy | sum of x·y |
8ACA | Corr | r | correlation coefficient |
8AD3 | MedX | Med | median of x |
8ADC | Q1 | Q1 | first quartile |
8AE5 | Q3 | Q3 | third quartile |
8AEE | QuadA | a | regression coeff a (highest order) |
8AF7 | QuadB | b | regression coeff b |
8B00 | QuadC | c | regression coeff c |
8B09 | CubeD | d | regression coeff d |
8B12 | QuartE | e | regression coeff e |
8B1B…8B50 | MedX1/2/3, MedY1/2/3 (8B1B/8B24/8B2D/8B36/8B3F/8B48) | Med‑Med (×3 partitions) |
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 F2–FF, §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 (F2–FF, 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 writesa,b,c,d,ethere in descending order of power._ErrStat(00:2741, id0x44C2, code0x15“STAT”) and_ErrStatPlot(00:2759, code0x1B) are the STAT‑specific error raisers; the_OneVarbody jumps to0x2741on e.g. fewer than the required data points._ErrDimMismatch(0x2715) is raised ifL1andL2/freq lengths differ (the21bblength compare at6584/658a).
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:
| Token | Value | Command | Model |
|---|---|---|---|
tOneVar | F2 | 1‑Var Stats | one variable |
tTwoVar | F3 | 2‑Var Stats | two variable |
tLR | F4 | LinReg(a+bx) | degree‑1 (a+bx form) |
tLRExp | F5 | ExpReg | y=a·bˣ (log‑linear) |
tLRLn | F6 | LnReg | y=a+b·ln x (log‑x) |
tLRPwr | F7 | PwrReg | y=a·xᵇ (log‑log) |
tMedMed | F8 | Med‑Med | resistant line |
tQuad | F9 | QuadReg | degree‑2 |
tLR1 | FF | LinReg(ax+b) | degree‑1 (ax+b form) |
CubicReg/QuartReg come in as the regression tokens tCubicR=2Eh/tQuartR=2Fh;
SinReg=32h, Logistic=33h, LinRegTTest=34h are 2‑byte t2ByteTok (0xBB)‑prefixed
tokens (their 2Eh/2Fh/32h/33h/34h values are the second byte after 0xBB). Degree for the polynomial solver = the model index; the coefficient
fan‑out into QuadA..QuartE is naturally sized by degree. [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). TheLD A,0x35/0x36andCALL 0x213dare 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 inQuadA(8AEE) downward. [confirmed] -
Correlation
randr²are computed for the linear models from the centred sums:$$r=\frac{\sum (x-\bar x)(y-\bar y)}{\sqrt{\sum (x-\bar x)^2\,\sum (y-\bar y)^2}}=\frac{n\sum xy-\sum x\sum y}{\sqrt{\big(n\sum x^2-(\sum x)^2\big)\big(n\sum y^2-(\sum y)^2\big)}}$$
assembled with
_FPMult/_FPSub/_SqRoot/_FPDiv(the6845/684ccluster) and stored toCorr(8ACA). The store offset is pinned: at3A:684Fthe code doesLD A,0x12 ; CALL 0x213D, and0x213Dis_Sto_StatVar(the store counterpart of_Rcl_StatVar 00:2149— both funnel through the0x3E07statVar dispatcher with the name id inA). Id0x12=tCorr= theCorrslot, so this single sequence is exactlyr → Corr (8ACA). The preceding3A:6845_SqRoot/_FPDivcluster forms the ratio;r²(andR²for higher‑order fits) is ther·r/ coefficient‑of‑determination derived from the same cluster and surfaced through the sameCorrslot. [confirmed: theA=0x12 → _Sto_StatVarr‑store; standard formula] -
The fitted equation is also written to
RegEQ(theY=‑style regression equation system var, recalled via tokentRegEq=0x01) soRegEQcan be pasted or graphed. [standard]
The Med‑Med model (F8) takes the resistant‑line branch (3A:760F/79B9):
it sorts, splits the x‑sorted data into three equal partitions, takes the median
(x,y) of each (MedX1/2/3, MedY1/2/3 at 8B1B…), and fits the line through the
outer two summary points adjusted toward the middle — classic Tukey median‑median.
[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/MaxXare tracked during the §4 pass (running min/max compares).- The median/quartile path (
3A:79B9→7A0B…) sorts a working copy via the internal sortstat_sort(3A:7935), then:Med(MedX,8AD3) = middle element (or mean of the two middle for even n),Q1(8ADC) = median of the lower half,Q3(8AE5) = median of the upper half (TI’s “exclude the overall median when n is odd” convention), with frequency‑weighted positions (the7B30/7B4C/7B6Ehelpers walk the cumulative‑frequency index, and198d/238binterpolate the rank). [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]
- Parser pushes the list args, sets
A = command token,bcall(_OneVar). _OneVarparses args → x‑list ptr(84D3), y‑list(84D5), freq(84DB); saves the model code to(8A36).- Accumulation pass (§4): one walk of L1/L2 building
n, Σx, Σx², Σy, Σy², ΣxyandminX/maxX/minY/maxYintostatVars, plus the 2×2 moment matrix. - 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$).
- 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=intercept→QuadA/QuadB;r,r²→Corr; equation →RegEQ, pasted intoY1. - Results displayed by the STAT‑CALC report screen; all of x̄/Σx/…/a/b/r persist
in
statVarsfor later recall by name (_Rcl_StatVar).
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:addr | name | what |
|---|---|---|
3A:6420 | _OneVar | STAT‑CALC entry (1/2‑Var + all regressions), id 0x4BA3 |
3A:6572 | onevar_accumulate | one‑pass power‑sum accumulation loop |
3A:6567 | onevar_powmul | running power·freq product (OP1→OP2, ×) |
3A:6345 | onevar_frame_teardown | restore stat error frame |
3A:6352 | onevar_frame_teardown_tail | on-error tail calling onevar_frame_teardown |
3A:6984 | stat_stddev_pop | population variance/σ finalize (÷ n) |
3A:6989 | stat_stddev_samp | sample variance/S finalize (÷ n−1) |
3A:6998 | stat_var_core | (Σx²−n·x̄²) variance core + √ |
3A:67C6 | reg_gauss_solve | Gauss‑Jordan solve of normal equations |
3A:69AF | reg_store_coeff | write a solved coefficient (matrix set) |
00:3A8F/3AA1/3AA7/3AAD/3AB9 | stat_mtx_index/get/set | RAM trampolines for sums-matrix element access by (row,col) |
3A:6F6A | stat_next_elem | fetch next list element, advance ptr |
3A:6F7D/6F90 | stat_freq_default | default frequency = 1 |
3A:7935 | stat_sort | stat-internal data sort (median/quartile, Med-Med) |
3A:79B9 | stat_median_quartile | median/Q1/Q3 + Med‑Med medians |
3A:760F/75E4 | medmed_partition | Med‑Med 3‑partition setup |
00:2149 | _Rcl_StatVar | recall a named statVar into OP1, id 0x42DC |
00:2741 | _ErrStat | raise STAT error (code 0x15), id 0x44C2 |
00:2759 | _ErrStatPlot | raise STAT PLOT error (0x1B), id 0x44D1 |
00:2294 | _Minus1 | OP1 − 1 (n→n−1 for sample stddev) |
33:65DC | _ZmStats | ZoomStat — fit window to plotted data, id 0x47A4 |
00:2715 | _ErrDimMismatch | list length mismatch (0x8B) |
RAM: statVars=0x8A3A, 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
rstore offset.3A:684FdoesLD A,0x12 ; CALL 0x213D(_Sto_StatVar, id0x12=tCorr), i.e.r → Corr (8ACA);r²/R²is ther·r/coefficient‑of‑determination from the same6845_SqRoot/_FPDivcluster, surfaced through the sameCorrslot (§5). (Residual: the6845–6891region is unanalyzed code in the DB, so only theA=0x12store 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_varsare separate command handlers, not reached through_OneVar(whose tokens are onlyF2–FF); noPStat‑writing routine is among the page‑0x3Astat_*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 viarst 28h(the bcall site isn’t fully analyzed in the DB). TheSortA(/SortD(command sort is a different routine (page 0x02, comparator_CpOP1OP2) — its complex-list ordering is documented in Matrices & 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 bycount× 9-byteTIFloatelements (18-byte complex elements if the list is complex, flagged0x0C). Element $i$ (1-based) lives at $\mathrm{addr}(L_i)=\mathrm{data}+2+(i-1)\cdot 9$. -
A matrix is
byte dim0; byte dim1;(two 1-byte dimensions) followed bydim0*dim1× 9-byteTIFloat, 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
TIFloatthroughOP1/OP2and the FP engine — there is no “vector unit”; matrix multiply is a triple loop of_FPMult+_FPAdd. -
The data area is found through the VAT (
_FindSym, doc 05): the VAT entry’s data pointer + page byte locate thecount/dimheader, after which all indexing is pointer arithmetic computed by_AdrLEle/_AdrMEle. -
One shared Gauss-Jordan engine (
02:42A6) implements matrix inverse[A]⁻¹(flag0x00) anddet((flag0x40) with partial pivoting.rref(/ref(are the same elimination family.
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 literallyresult = H * L(B=H; HL=Σ L, aDJNZadd loop) — it computes the element count from the two dimension bytes. [confirmed]- Header = two bytes
dim0,dim1; data isdim0*dim1floats 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’sB = idx0 = column,C = idx1 = row._CreateRMat(ram:1115) confirms the layout: it isPUSH HL ; CALL _HTimesL (1EF6) ; LD A,2 ; JR 10DD—_HTimesLreturnsH·L(the element count) andA=2is the 2-byte dim header; the two dimension bytes are storeddim0(rows) thendim1(cols). The byte-confirmed index arithmetic((idx0−1)·dim0 + (idx1−1))·9is 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];_CkValidNumvalidates 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_CkOP1Posbounds (loadsA=0x15=E_Statand jumps to the error vectorram:2741on 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 (_AdrMElethenRST4= load 9 bytes)._PutToMat(02:406C) =mele_store_ckvalid(02:4068):_AdrMEle ; _CkValidNum ; _MovFrOP1— OP1 →[M](r,c)with validation._StMatEl(38:6C8F) — high-level “store into[M](r,c)” used by the parser: resolves the matrix name (5F45), bounds-checks indices against the dims (r≤rows && c≤cols, else_JError 0x8C=E_Dimension), unarchives if needed, then_PutToMat. [standard]
Internal index helpers reused by the algorithms [confirmed]
mele_adr_af_jp(02:403C) =_AdrMEle(currentIJ) ; RST4— “load[M](i,j)to OP1” (the elimination inner-loop read). Indices come from the loop state at84AF/84B3/84B4.mele_adr_to8483(02:4051) =_AdrMEle ; _Mov9B(→OP2@8483)— load element to OP2.mele_put_af(02:405A) /mele_put_d3(02:405E) =_AdrMEle ; _CkValidNum ; _MovFrOP1— store OP1 back to[M](i,j)._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
| Routine | addr | Role |
|---|---|---|
_CreateRList | 00:10C4 | new real list: count*9+2 bytes (§1) [confirmed] |
_CreateCList | 00:1109 | new complex list: count*18+2 [confirmed] |
_IncLstSize | 07:4EF4 | grow a list in place via _InsertMem; caps length at 999 (0x3E7), else E_Dimension 0x8C (07:4F00 JP Z,0x2719 → LD A,0x8C). _InsertList is the distinct sibling at 07:4F07. [confirmed] |
_DelListEl | 07:4F43 | delete element(s): _HLTimes9(index) to size the gap (×2 if complex, & 0x1F == 0x0D), then _DelMem via a cross-page jump [confirmed] |
_RedimMat/_ConvDim | 07:4D3B / 38:741F | re-dimension (shared with matrices); _ConvDim/_ConvDim00 (38:741F/7422) coerce OP1 to a real index first [confirmed] |
dim(, dim(L)→n, list↔value
dim( reads the count word straight from the list header; assigning n→dim(L) calls the
resize path (_IncLstSize/_DelListEl) to grow/shrink, zero-filling new cells. List→matrix
and matrix→list (List►matr(, Matr►list() reshape via _DataSize + 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])evaluatesexprforvar = lo..hi, pushing each result and finally_CreateRList-ing the collected floats (the generic list-builder loop;_SetSeqM 36:7D1Fis the sequence-graph variant). [standard]cumSum(is a running_FPAddwriting back each partial sum (the sum-fold with the accumulator stored every step). [hypothesis]SortA(/SortD(— list sort in place (SortA(co-sorts dependent lists); the comparator and per-element sort key are detailed in the next subsection. [confirmed comparator]- Stats (
mean/median/sum/stdDev/variance) are list folds layered onsum(/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 element | Sort key |
|---|---|
| real | the value (sign → magnitude) |
| complex | the real part only; the imaginary part is not read, and elements with equal real parts keep their input order |
No element type is ordered by magnitude/modulus (_CAbs is never on this path). [comparator and
its real-number semantics confirmed; the per-element sort key follows from them — the unanalyzed
sort body’s element-load is not byte-traced]
Traceable list sample
The tools/tibasic-samples/data.* fixture drives the list paths above with a
small end-to-end TI-BASIC program:
{3,1,4,1,5}->L1
SortA(L1)
cumSum(L1)->L2
sum(L1)->S
Disp L1
Disp L2
Disp S
It exercises list literal creation, list variable tokens (5D 00/5D 01),
in-place sorting, a running cumulative sum, a folded sum, and list display. The
generated DATA.8xp was run under headless TilEm: the screen showed sorted
L1={1 1 3 4 5}, cumulative L2={1 2 5 9 14}, and sum 14; the trace hit
list_fold_dispatch (02:6104) plus the page-38 list parse/store helpers.
[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)(token0xB4→identity_build(02:4108)) [confirmed]: allocaten×n, then walk every cell writing1.0whenrow==col(theexp==typetest) and0otherwise:_OP1Set1 ; for each (i,j): if i==j -> store 1.0 (mantissa[0]=0x10) else 0Fill(value,[M])/randM(stamp a constant / random values across all cells via a per-cell loop over the whole matrix. The02:62D4branch (CP 0xB5) isdim((0xB5=tDim), which creates ther×cresult (5DBB→_CreateRMat 110F) and stores the dims (631B/631C/4825) but performs no fill.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_Randombcall (0x4B79): a ROM-wide scan finds zeroRST 28h; .dw 0x4B79sites, 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 412A–414E family and is inferred here. [standard]
[A] * [B] — matrix multiply [confirmed]
The multiply body is at 02:40BA. It is not a defined function in the disassembly (so the
decompiler/MCP can’t reach it), so this was decoded from rom.bin directly with z80dasm,
cross-checked against a routine Ghidra does define. The body is called from 02:5FFF (the
* operator handler, in the 02:5FE6 region) and reused from 02:4605 and 02:5B39. (0x40BA
is also the _SinCosRad bcall ID in ti83plus.inc — a hex coincidence, unrelated to this page-02
address.)
40BA is a classic O(n³) triple loop with an FP accumulator:
for each result cell (i,j): ; counters at 84B7, 84B4
for k = 1 .. inner: ; inner counter at 84AF
load [A](i,k) (403C mele_adr_af_jp)
multiply by [B](k,j) (47B9 / 0166F FP multiply)
accumulate (479F)
store acc -> [C](i,j) (4064 / 405A)
The three dec (hl) counters (84AF inner, 84B4, 84B7) each have a jr nz back-edge
(40E5, 40F9, 4100); an inner-dim mismatch (A.cols ≠ B.rows) raises _ErrDimMismatch. An
n×n product is n³ TIFloat multiply+add steps. [confirmed — 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 414A–4178 block,
not the transpose body. [confirmed]
augment(, dim(, List►matr(, Matr►list( — per-function drivers [standard]
These are dispatched from the page-02 function-token evaluator (list_fold_dispatch, the
CP imm ; JR/JP chain that runs 5E46/60C8–63xx, keyed on the token byte). Each command’s
body and its single caller are byte-verified below.
| Command | dispatch site | body | what the disassembly shows |
|---|---|---|---|
Matr►list( | 0x8D @ 6388 | 02:4773 (2-arg), 02:49E3 (1-arg list copy) | [confirmed] The 0x8D branch splits on argument count (638D: CP 0x02). The column-extract engine is 02:4773 (2-arg path: 639D: CALL 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 @ 60E9 | 02:412A | [confirmed] Swaps the dim header (60F5), allocates the transposed shape, then 412A copies dst(c,r)=src(r,c) over every cell (403C read from (84D3), 4068 write to (84D7)); only caller 60FE. See the transpose subsection above. |
augment( | 0x91 @ 635B | 02: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 (5DE0 → 5DE6 → _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 @ 62D4 | create + dim setup (5DBB/5DEB) [confirmed] | The compare at 62D4 is CP 0xB5, and 0xB5 = tDim (dim(), not randM( — so this is the →dim( matrix create/resize handler. It splits on argument count (62D9: CP 0x02): a 2-arg path (62DD) and a 1-arg path (630A). Both create the result and set its dims through 02:5DBB (CALL 5CEB registers the variable by name, stores the data pointer to 84D3, reads and zero-rejects the dim bytes OR 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 @ 61C1 | 02:7D19 + copy | reshapes the argument lists into a matrix (_DataSize-counted float copy 4539/453F). [standard] |
The matrix-element kernels these drivers share are _AdrMEle/_AdrMRow (4002/4000) for indexing,
4068 (mele_store_ckvalid) for validated stores, and 4539 (mele_copy9_d3) for the bulk
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 / op | site | flag A | meaning |
|---|---|---|---|
[A]⁻¹ (^ token 0x0C, operand = matrix) | 02:5F80 | 0x00 | inverse; singular ⇒ error |
det( (token 0xB3) | 02:5FC0 | 0x40 | determinant; bit6 set ⇒ singular tolerated (returns 0) |
det(’s handler at 02:5FA3 (not a defined function in the disassembly; address
unverified) first type-checks the operand is a matrix (chk_op_is_matrix (02:69B7):
type==2 else E_DataType 0x89), then LD A,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]
461Cmat_max_abs— compute the matrix’s max-abs element (numeric scale for the near-zero pivot test).41C1abs_cmp_op1op2—|OP1|vs|pivot|compare (1A0F/1987abs+compare);41D0— scan a column for the largest-magnitude pivot (partial pivoting), calling43B9to swap rows as it goes.43B9/414Emrow_swap_loop/_AdrMRow— physical row swap / row scale (whole-row moves;414Eloads thedim0stride and swaps two whole rows via_AdrMRow×2 +1DDA).4259— swap two entries in the permutation vector at84D5.4473ele_sub_ref— the elimination element step ([M](i,k) − factor*[M](pivot,k):RST8 ; CALL 403C ; JP 2297= load +_FPSub).426Dcol_dot_accum/426Fcol_dot_accum_from— column dot-product / back- substitution accumulate (_FPMult+RST6).- Pivot normalize uses
_FPRecip/_FPDiv; sign/inverse use_InvOP1S.
det( therefore = forward elimination with partial pivoting, return the signed product
of the pivots (each row swap flips the sign); a zero pivot ⇒ det = 0 (no error).
[A]⁻¹ = full Gauss-Jordan (reduce to identity, the augmented identity becomes the
inverse); a zero pivot ⇒ ERR:SINGULAR MAT.
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 intoOP1/OP2(RST4= load-9,_Mov9B,_MovFrOP1) and all arithmetic is the FP engine’sRST 30h(_FPAdd)/_FPMult/_FPDiv/_FPSub/_FPRecip. There is no SIMD; a matrix multiply is literally thousands of these calls. Complex elements (lists/[i]) carry a0x0Cflag 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). Thecount/dimheader is read first; then_AdrLEle/_AdrMEledo pointer math. A store into an archived matrix/list unarchives to RAM first (_Arc_Unarc; Flash cannot be written in place). - Scratch RAM used by the algorithms (verified operands):
84AF(current dims / i,j loop state),84B0/84B3/84B4(pivot, k, row counters),84B7(dims copy),84D3/84D5/84D7(data pointers + the permutation vector base),8478=OP1,8483=OP2,8499=OP4,84AF=OP6 region = the matrix-op loop frame.
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 code | name | raised by |
|---|---|---|
0x78 | 0-index reject (via ram:2793) | _AdrMEle/_AdrMRow on a 0 row/col index |
0x83 | E_SingularMat (ERR:SINGULAR MAT) | 42A6 inverse on a zero pivot (_ErrSingularMat 00:26F0) |
0x85 | E_Increment | _ErrIncrement 00:26F8 (bad seq/loop step) |
0x89 | E_DataType | det(/matrix ops on a non-matrix operand (chk_op_is_matrix (02:69B7)) |
0x8B | E_DimMismatch (ERR:DIM MISMATCH) | add/sub/multiply with incompatible dims (_ErrDimMismatch 00:2715) |
0x8C | E_Dimension (ERR:INVALID DIM) | non-square det/inverse, out-of-range element store (_ErrDimension 00:2719, _StMatEl) |
0x15 | E_Stat (via ram:2741) | _GetPosListElem bad index (_CkOP1Pos) |
8. Confident address index
| space:addr | name | what |
|---|---|---|
00:10C4 | _CreateRList | new real list (count*9+2) [confirmed] |
00:1109 | _CreateCList | new complex list (count*18+2) [confirmed] |
00:1115 | _CreateRMat | new matrix (H*L*9+2, header dim0,dim1) [confirmed] |
00:1EF6 | _HTimesL | element count = H*L (dims multiplied) [confirmed] |
00:1930 | _HLTimes9 | ×9 (real TIFloat stride) [confirmed] |
02:4000 | _AdrMRow | address of matrix column start (column stride) [confirmed] |
02:4002 | _AdrMEle | matrix element address: ((column-1)*dim0+(row-1))*9 [confirmed] |
02:4044 | _GetMToOP1 | [M](i,j) → OP1 [confirmed] |
02:406C | _PutToMat | OP1 → [M](i,j) (validated) [confirmed] |
02:40BA | matrix-multiply body | O(n³) triple loop, decoded from rom.bin (not a defined function in the disassembly); called from 02:5FFF/4605/5B39 (§4). 0x40BA in ti83plus.inc is the unrelated _SinCosRad bcall ID. [confirmed] |
02:4108 | identity_build | identity(n): diagonal-1 fill (token 0xB4) [confirmed] |
02:412A | mat_transpose | transpose [A]ᵀ body (token 0x0E, dispatched 60E9/called 60FE): per-cell copy dst(c,r)=src(r,c) via the swapped dest header (§4) [confirmed] |
02:414E | mrow_swap_loop | row swap/scale (elimination) [confirmed] |
02:4178 | mat_fill_type1 | live DB name; single-counter per-cell fill/apply loop in the 414A–4178 block — not transpose (§4) [confirmed] |
02:4539 | mele_copy9_d3 | bulk column-major float-payload copy (skip 2 dim bytes, LDIR); used by augment(/reshape (§4) [confirmed] |
02:4663 | mat_gauss_engine | live DB name; min(H,L) partial-pivoting elimination engine; only caller is the augment( 0x91 branch (6379). Its role inside plain augment( is the one open item (§4) [standard] |
02:4773 | mat_to_list_cols | Matr►list( 2-arg column-extract engine (only caller 63A0): nested col×row walk copying matrix columns into list element(s) (§4) [confirmed] |
02:5264 | cplx_swap_dispatch | live DB name; complex OP-pair arrange/swap (5344/52D3) reached only from the 0xBD branch (62D0) — not the 0xB5/dim( matrix-create branch (§4) [confirmed] |
02:6238 | mat_augment_copy | augment( column-concat: allocate result (5DE0) + 4539 payload copy + re-point 84D3 (§4) [confirmed] |
02:49E3 | lele_copy_until_eq | live DB name; list-element copy-until-length-match (21BB, RET Z); inner copy of the Matr►list( 1-arg/list path (6397) (§4) [confirmed] |
02:41C1 | abs_cmp_op1op2 | absolute-value compare: OP1 vs pivot [confirmed] |
02:41D0 | pivot_col_scan | partial-pivot: find largest absolute value in column [confirmed] |
02:4259 | perm_swap | swap two entries of the permutation vector (84D5) [confirmed] |
02:426D/426F | col_dot_accum/col_dot_accum_from | column dot-product / back-substitution accumulate [confirmed] |
02:42A6 | matrix_gauss_engine | inverse(flag 0)/det(flag 0x40) Gauss-Jordan + partial pivot; square-only (H==L guard) [confirmed] |
02:4473 | ele_sub_ref | [M] − factor*pivot element step (_FPSub) [confirmed] |
02:461C | mat_max_abs | maximum absolute element (pivot tolerance) [confirmed] |
02:47C5 | _AdrLEle | list element address: data+2+(i-1)*9 [confirmed] |
02:47EA | _GetLToOP1 | list[i] → OP1 (complex-aware) [confirmed] |
02:47FB | _RclListElemToOP1 | recall list elem to OP1 [confirmed] |
02:47FE | _RclListElemB | recall list elem (B-indexed) [confirmed] |
02:4829 | _PutToL | OP1 → list[i] (validated, complex-aware) [confirmed] |
02:49A7 | _RclCListElem | complex-list element → OP1/OP2 [confirmed] |
02:49B5 | _RclCListElemB | complex-list element (B-indexed) [confirmed] |
02:5BBB | _GetPosListElem | list element by positive index (bounds) [confirmed] |
02:5E46 | func_eval_dispatch | single-byte function-token evaluator (0xB0–0xCD) [confirmed] |
02:5F80 | mat_inverse_entry | [A]⁻¹: flag 0 → matrix_gauss_engine [confirmed] |
02:5FC0 | det_entry | det(: flag 0x40 → matrix_gauss_engine [confirmed] |
02:6104 | list_fold_dispatch | sum(/prod( higher-order list fold [confirmed] |
02:69B7 | chk_op_is_matrix | require operand type==2 else E_DataType [confirmed] |
ram:21C4 | chk_type_lt_1a | classify element type width: AND 0x1F ; CP 0x1A ; CP 0x18 ; CCF — real-vs-complex (0x0C) element width [confirmed] |
35:79E9 | _ListIdxTimes9 | list index ×9 + dispatch [confirmed] |
07:4D3B | _RedimMat | re-dimension matrix/list [confirmed] |
07:4F07 | _InsertList/_IncLstSize | grow a list in place [confirmed] |
07:4F43 | _DelListEl | delete list element(s) [confirmed] |
38:6C8F | _StMatEl | parser store into [M](r,c) (bounds-checked) [confirmed] |
38:741F/7422 | _ConvDim/_ConvDim00 | coerce a dim/index to real [confirmed] |
00:26F0 | _ErrSingularMat | E_SingularMat 0x83 [confirmed] |
00:26F8 | _ErrIncrement | E_Increment 0x85 [confirmed] |
00:2715 | _ErrDimMismatch | E_DimMismatch 0x8B [confirmed] |
00:2719 | _ErrDimension | E_Dimension 0x8C [confirmed] |
9. Findings
rref(/ref(use a separate driver, not42A6. Xref proves42A6has exactly two callers (inverse5F80, det5FC0); rref/ref are 2-byte0xBB-lead function tokens dispatched via the page-38 evaluator’s class-3 handler table at38: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 be42A6.- det sign / pivot-product (
42A6tail43D8–4470) and dim labelling. The det sign = LSB of the permutation-swap count applied via_InvOP1S(24BD) at43FB/442B; the magnitude is the238B/RST 30hdiagonal-pivot accumulate (43E3–43F6);420F/4259undo the column permutation for the inverse (§5). Row/col vs dim0/dim1 is now [confirmed]:dim0(first header byte) = #rows, and_AdrMEletakesB=column,C=row(§1/§2). - transpose,
Matr►list(, and theaugment(column-concat bodies. Each command’s page-02 dispatch site and body are byte-confirmed, every body having exactly one caller (§4):- transpose
[A]ᵀ(token0x0E@60E9) →02:412A(only caller60FE): the dim header is swapped (60F5) and412Acopiesdst(c,r)=src(r,c)over every cell.02:4178is a separate single-counter fill/apply, not transpose. [confirmed] Matr►list((0x8D@6388) →02:4773(2-arg column-extract engine, only caller63A0) with02:49E3as the 1-arg/list inner copy. [confirmed]augment((0x91@635B) → equal-rows guard (CP L ; JP NC,2719) + column-concat copy at02:6238(5DE0allocate +02:4539LDIRpayload copy). [confirmed]dim((0xB5@62D4;0xB5=tDim, notrandM() → creates the result and sets its dims (5DBB/5DEB).02:5264(cplx_swap_dispatch, only caller62D0in the0xBDbranch) is reached only from that complex branch, not here. [confirmed]List►matr(0x8Ebranch (61C1) →02:7D19+_DataSizecopy (4539/453F) is unchanged [standard].
- transpose
- OPEN — two residuals inside the confirmed branches (§4):
augment(’s0x91branch calls02:4663(mat_gauss_engine, only caller6379) — amin(H,L)partial-pivoting elimination pass — after the column-concat copy. Its role for plainaugment(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 the0xB5/dim(branch; the visible matrix-create/dim-convert code isdim(’s, not randM’s. randM does not go through the_Randombcall (0x4B79) — a ROM-wide scan finds noRST 28h; .dw 0x4B79site. [standard]
seq(/SortA(/SortD(/stats list-builders: confirm the collect-then-_CreateRListloop and the in-place float sort/compare. (Residual — comparator_CpOP1OP2confirmed; the unanalyzed page-02 sort body’s element-load is still not byte-traced.)
Solver & 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.
| Error | bcall | page-0 stub | code | Message |
|---|---|---|---|---|
_ErrSignChange | 0x44C5 | ram:2749 → _JError(0x98) | 0x98 | NO SIGN CHNG |
_ErrIterations | 0x44C8 | ram:274D → _JError(0x99) | 0x99 | ITERATIONS |
_ErrBadGuess | 0x44CB | ram:2751 → _JError(0x9A) | 0x9A | BAD GUESS |
_ErrTolTooSmall | 0x44CE | ram:2755 → _JError(0x9C) | 0x9C | TOL NOT MET |
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:
_CkValidNum(ram:1E9B), then_MovFrOP1(ram:1B0C) stores the current guess into the solve variable (its data pointer is loaded from(9306), in the expression-stack region — the bytes areED 5B 06 93=LD DE,(9306)).- It installs 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 at39:327Fusingparse_advance 7248/parse_cur_tok 72DA-style cursors, mirroring the page-0x38 parser — the equation is re-tokenised and evaluated every iteration). - The post-eval filter at
39:46C7inspects the error code inA: codes below0x86(OVERFLOW/DIV BY 0/SINGULAR MAT/DOMAIN) and0x87(NONREAL ANS) are swallowed (CP 0x87; JR ZthenCP 0x86; JP NC,0x2799— thisxis treated as a point wherefis undefined, so the solver can step past it) while0x86(BREAK) and codes≥ 0x88are re-raised via_JErrorNo(JP 2799). This is whysolve(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 Aeach pass (39:44BF), pushed on the stack. Two caps are compared withSBC HL,…:LD HL,0x01F3(=499) at39:4479/39:458B→ exceeding it jumps to39:45A0 LD A,0x99 … JP 2793= ITERATIONS, and the earlyLD A,0x9Apath (39:45AD) = BAD GUESS (raised when the initial bracket is unusable).- A small count (
CP 0x04,39:44C3) gates the early Illinois/secant correction.
- Bisection midpoint:
_InvSub(ram:227D, = b−a) then_TimesPt5(ram:2382, ×0.5) give the half-width $\tfrac{1}{2}(b-a)$ at39:443C/443F; adding $a$ yields the midpoint $m=a+\tfrac{1}{2}(b-a)$. [confirmed] - Secant / regula-falsi step:
_FPMult(238B),_FPSub(2297),_FPDiv-class and_InvOP1S(24BD) around39:4488…44F2compute the linear-interpolation step $x_{n+1}=x_n-f(x_n)\,\dfrac{b-a}{f(b)-f(a)}$. The result is compared against the bisection bound; the algorithm keeps the secant guess only if it stays inside the bracket, otherwise it falls back to the midpoint — a classic bisection ⊕ secant (Illinois/regula-falsi) hybrid, the documented TI behavior. [confirmed for the op sequence; method name standard] - Sign-change bookkeeping: the byte at
0x84AF(OP6 area) holds the running sign offat the bracket ends;XOR 0x80toggles it (39:44AB…44B3). If the two bounds never bracketed a sign change, the path at39:45CD…45DA JP 2749raises NO SIGN CHNG. [confirmed] - Convergence / tolerance test:
_AbsO1O2Cp(ram:1987, compares |OP1| vs |OP2|) is used repeatedly (39:446F,44D7,44F8,45C7) to test the bracket width / residual against tolerance. The tolerance floor is the TIFloat constant1.0e-13at39:46EA(00 73 10 …); the residual-zero floor is1.0e-99at39:46E1(00 1D 10 …). On reaching tolerance the solver exits through the39:4540 → 4553branch (dynamically traced on anX²−2 = 0solve that converged to √2 ≈ 1.41421356);39:4547is aCALL, not the converged return, and the observed path bypassed it. The tolerance tests at446F/44D7/44F8run under that trace;45C7is reached only on other convergence sub-paths. [confirmed]
\begin{algorithm}
\caption{Solver root-finder --- bracketed secant / regula-falsi (page 0x39)}
\begin{algorithmic}
\REQUIRE bracket $[a,b]$ with $\mathrm{sign}(f(a)) \neq \mathrm{sign}(f(b))$ \COMMENT{else \textsc{no sign change} (0x98)}
\FOR{$k = 0$ \TO $499$}
\STATE $m \gets a + \tfrac{1}{2}(b-a)$ \COMMENT{bisection midpoint: \texttt{\_InvSub}, \texttt{\_TimesPt5}}
\STATE $s \gets a - f(a)\,\dfrac{b-a}{f(b)-f(a)}$ \COMMENT{secant: \texttt{\_FPMult/\_FPSub/\_FPDiv}}
\STATE $x \gets s$ \textbf{if} $s \in [a,b]$ \textbf{else} $m$ \COMMENT{fall back to bisection}
\STATE $f_x \gets \mathrm{eval\_equation}(x)$ \COMMENT{re-parse, error-trapped (39:468F)}
\IF{$\mathrm{sign}(f_x) = \mathrm{sign}(f(a))$}
\STATE $a \gets x$ \COMMENT{keep the sign change in the new bracket}
\ELSE
\STATE $b \gets x$
\ENDIF
\IF{$|b-a| < 10^{-13}$}
\RETURN $x$ \COMMENT{converged; exits via 39:4540 -> 4553}
\ENDIF
\ENDFOR
\STATE \textbf{raise} \textsc{iterations} (0x99) / \textsc{bad guess} (0x9A)
\end{algorithmic}
\end{algorithm}
Dynamic confirmation. Traced end-to-end under headless TilEm by driving the built-in Equation Solver to solve
X²−2 = 0(solver-sqrt2.macro). It converged on screen toX = 1.4142135623…(√2) withleft-rt = 0. The mem-write records show the guess at0x8478climbing1.40898 → 1.41421335 → 1.4142135623645 → 1.4142135623731(|err| ≈ 4.9e-15, crossing below the1e-13tolerance on the final step).solver_iterate(39:4413) ran 808×; the per-iteration re-parse (parse_eval_expr38:5AB3) ran 834×; the secant-in-bracket-else-bisect test (39:44F8), the499-cap compare (39:4479 LD HL,0x01F3), and the1e-13/1e-99constants (39:46EA/46E1) all executed as the pseudocode describes.
left-rt shown on the Solver screen is the final residual f(root) (the
left-side − right-side value the evaluator computed). [standard]
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:7F02loads the pointer at(84D3)(iMathPtr1;ED 5B D3 84=LD DE,(84D3)),3A:7F0Fthe one at(84D5)(iMathPtr2), etc. ((D?5B)are the finance sysvar VAT slots).(84D3)=iMathPtr1,(84D9)=iMathPtr4,(84AF)=OP6,(84D3)/(84D9)/(84D3)hold the iteration state. [confirmed]
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; _AllocFPSat3A:70A2) and the loop counter isB = 0x40(=64iterations max),3A:70AB. - Each pass recomputes the TVM residual and its derivative, takes a Newton step, and tests
the exponent of the correction against
CP 0x74(3A:71F4) — i.e. converged when the update is ≤ ~10⁻¹². The new estimate is written back via(84D9)→(84D3)(3A:71F9…71FE). [confirmed]
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 Bbudget falls to3A:7206 JP 274D= ITERATIONS (0x99). Solving forN/PV/PMT/FVis 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 2ε 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 at33:4D18are executable code —33:4D18 21 83 84(LD HL,0x8483),33:4D1B 3E 60(LD A,0x60),33:4D1D CD 65 1B(CALL fp_set_digit1B65; not_OP2SetA, whose body is1B24) — loading the scalar0x60 = 96(a working digit/scale count), not a quadrature weight. [confirmed bytes] - maintains a working set of partial sums in an FPS frame (
_AllocFPS 1534,_PopRealOx 14F6/150F/1505,_DeallocFPS 1526, with slot offsetsDE=0x15/0x1B/0x24) — endpoint values, the running estimate, and the previous estimate for the error test. [confirmed] - iterates, refining the partition by interval bisection (the ×0.5
_TimesPt5halving; the97E7/84AFdepth counters track subdivision depth; the loop tail is33:4E81 LD DE,0x0024 … C3 CB 45and33: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 to33: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:
- Place the trial value in
OP1(_Mov9ToOP1/ arithmetic result). _MovFrOP1(ram:1B0C) store it into the named variable the expression mentions (the solve var, thenDeriv/fnIntintegration var, or the TVM var).- Re-evaluate the expression through the TI-BASIC parser (
_ParseInp 38:5987/parse_eval_expr 38:5AB3/_Find_Parse_Formula 38:758A; the Solver uses its own page-0x39 token walker). The parser walks the same stored token stream each pass. - Read the numeric result back from
OP1, form the residual / difference, decide the next step. 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:
- The evaluator hands the operand token to the page-0x02 dispatcher, which recognises the
0xBBgroup and the second byte:tFnIntat02:68F3(CP 0x24),tNDerivat02:6904(CP 0x25),tRootat02:58AD/02:69BC(CP 0x22). [confirmed bytes] - The page-0x02 handler parses the comma-separated argument list and sets defaults — e.g.
the
nDeriv/fnIntprologue at02:6AF6doesLD A,0x7D; LD (8479),A, seeding the default tolerance exponent0x7D(=1e-3, the documented nDeriv ε) before the call. [confirmed] - It then performs a paged call into page 0x33. The page-0x33 entry re-validates the token
through the
33:504Ebb_token_scanner(CP 0xBB, thenCP 0x68 / 0xCF / 0xDB / 0xF6to assign a small class index inCandCALL 0x50AC) and dispatches into the numeric bodiesnderiv_body(33:4C80) /fnint_body(33:4D00). Because the call crosses pages through the bcall/app-call trampoline, no static xref to these bodies survives in the Ghidra database — the mark of a generic paged call rather than an inline bjump. [confirmed 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)·100at33: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:4D1Bis executable code (LD A,0x60; CALL fp_set_digit). - TVM
_SinH(id 0x40CF) (§2.3). The TVM rate loop calls_SinHat3A:710B(0x40C6/0x40CF/0x40EDare three distinct hyperbolic bcalls); it evaluates the annuity / compound factor in hyperbolic form for numerical stability at small rates. - class-3 routing of
tFnInt/tNDeriv/tRoot(§4.1). Path isBB-token → page-0x02 dispatcher (02:68F3/6904/58AD) → arg-parse + default-tol (02:6AF6, exp 0x7D = 1e-3) → paged call → page-0x33 bodies, re-validated bybb_token_scanner(33:504E). The trampoline hides the static xref, confirming it is a generic paged call. - page-0 helper cluster (§5). Generic FPS slot save/restore (9-byte
TIFloat slots at
-(9*slot)from frame base(9302)) plus the active-frame swapper atram:2800, renamedfps_swap_active_frame; the store/load stubs arefp_st_slotN_opX/fp_ld_op1_slotN.
Residual (genuinely unverified, would need deeper paged tracing):
- The exact byte layout of the For/While/Repeat loop-control record pushed by the
page-0x33 control-flow handlers (
33:4381jump table) is not yet field-mapped; only the dispatch path is confirmed. (See sub-tibasic.md §4.) - bcall
0x462Ain 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 theTITokenenum, built from thet-prefixed equates inti83plus.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:
| Byte | Meaning (.inc) |
|---|---|
5C | tVarMat — matrix name ([A]…) |
5D | tVarLst — list name (L1…) |
5E | tVarEqu — equation variable (Y1, r1, …) |
60 | tVarPict — picture |
61 | tVarGDB — graph database |
62 | tVarOut — Y-vars / output |
63 | tVarSys — system var group (Xmin, …) |
7E | graph-format token group |
BB | t2ByteTok — the general “extended commands” page (2.x additions) |
AA | tVarStrng — string variable (Str1…) |
EF | TI-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 Str1–Str0 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 callssmallfont_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 andDisp).
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 callsparse_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 generatedASMPARSE.8xp/ZZPARSE.8xpfixture reaches it but fails withERR:INVALIDwithout running the named BASIC target. [confirmed negative probe]_Find_Parse_Formula(38:758A) —_FindSyma variable then parse its stored formula (Y-vars, equations). The generatedASMFORM.8xp/ZZFORM.8xpfixture reaches it from anAsmPrgmpayload but fails withERR:UNDEFINEDwithout 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 reloadsBCfrom it). The bounds/refill check is the adjacent entry38:7245, which calls0x1FD6before falling into the increment.parse_expect_or_err(38:5CD8) — fetch a token and raise_ErrSyntax(recording the position inparsePtr) 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. _ParseInp → parse_init → parse_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.
| Program | Source | Body bytes |
|---|---|---|
| Hello | ClrHome / Disp "HELLO, WORLD" | E1 3F DE 2A 48 45 4C 4C 4F 2B 29 57 4F 52 4C 44 2A 3F |
| Factorial | Prompt N / 1->F / For(I,1,N) / F*I->F / End / Disp F | 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 |
| Data | {3,1,4,1,5}->L1 / SortA(L1) / cumSum(L1)->L2 / sum(L1)->S / display results | 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 |
Asm( wrapper | Disp "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 bridge | Asm(prgmASMSIG) / If Ans / prgmZZBASIC | DE 2A 42 45 ... 72 3F 5F 5A 5A ... (full body in tools/tibasic-samples/asmbridge.tok) |
| ASM return value | Asm(prgmASMVAL) / Ans+3->A / Disp A | BB 6A 5F 41 53 4D 56 41 4C 11 3F 72 70 33 04 41 3F DE 41 3F |
| Animation | ClrHome / 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 drawing | ClrDraw, window stores, visible axes/diagonal, Circle(47,31,10), Text(0,0,"DFS"), DispGraph | 85 3F 30 04 63 0A ... DF 3F (full body in tools/tibasic-samples/graphviz.tok) |
| Graph visualization | ClrDraw, window stores, then Line(/Circle(/Text( drawing the DFS topology | 85 3F 30 04 63 0A ... DF 3F (full body in tools/tibasic-samples/graphdfs.tok) |
| List-driven graph visualization | edge endpoint lists L1–L4, 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 subprogram | 0->A / prgmSUBRT / Disp A; callee Disp "SUB" / A+1->A / Return | caller 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 fixture | caller initializes L1 and Ans, calls prgmABISUB, then displays A, L1, and Ans | caller 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 add | list digits in L1/L2, carry C, indexed stores into L3 | 08 35 2B 34 ... DE 5D 02 10 36 11 3F (full body in tools/tibasic-samples/bigadd.tok) |
| Big integer multiply | nested 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) |
| DFS | edge lists L1/L2, visited L3, stack L4, While/If Then/nested For | 08 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), viasmallfont_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;Dispof 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):
| Helper | Addr | Behavior |
|---|---|---|
parse_cur_tok | 38:72DA | fetch token at cursor (parsePtr); special-cases : token 0x3E (tColon)/end 0x00 |
parse_advance | 38:7248 | parsePtr (965D = nextParseByte) ++, bounds-check vs parseEnd (965F = basic_end); refill via deref_byte (38:5B79) |
parse_expect_or_err | 38:5CD8 | fetch a token; if not the expected one, set parsePtr to the fault position and _ErrSyntax |
parse_scan_tokens | 38:4180 | skip forward to a delimiter, honoring 2-byte tokens via _IsA2ByteTok; used by every block scanner |
parse_init | 38:5B7B | zero 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 RAM9305(official equateEST, edit-screen height), callsparse_init, clears an editing flag (*(IY+0x1F) &= 0xF7), then tail-calls_ChkFindSymto resolve OP1. [confirmed]_Find_Parse_Formula(38:758A)_FindSyms a named var then parses its stored formula; its body switches on var type (0x0FWindow /0x10ZSto /0x11TblRng 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 at38:59C8, insideparse_eval_expr) and the same precedence selector (the label at38: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 intoAns(_CkOP1Realpath; the bytes that follow are the Ans-var token table)._RclAns(38:679F) =_AnsNamethen_RclVarSym._AnsName(38:74B7):_ZeroOP1; (OP1+1)=0x72(OP1 holds a name here, so the byte at0x8479is the name’s type/class tag rather than an exponent) — builds the OP1 name for theAnsvariable (token class0x72)._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
Ifstatement handler evaluates the condition into OP1 (real). If the next token is nottThen, it’s a single-statementIf(execute the one statement when true, skip it when false). IftThen, it’s a block. - The Else path is
if_else_skip_handler(38:5826): on seeingtElse(D0)it repeatedly calls the block matcherblockmatch_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 at38:59C8. OthertElsecompares at38:57B3/58A6/58C6handle the symmetric “skip Then, run Else” and nested cases. if_isg_stmt_handler(38:6F63) is the per-statement entry that special-casestIf(0xCE) andtISG(0xDA,IS>(): the second compare is38:6F6C: CP 0xDA(tISG, the adjacent token totStop0xD9). FortIfit sets grammar class0x5Fand 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 above0xF5(the reserved/newer-token range —0x30is the message-table index, one below0x31ARCHIVE FULL). [confirmed bytes]
For( / While / Repeat / End [confirmed]
For(/While/Repeatpush a loop-control record onto the FPS/loop stack recording the loop variable, limit, step, and theparsePtrof the loop top (soEndcan jump back).Endpops/updates: increments theForvariable, re-tests the limit, and either re-seedsparsePtrto the loop top or falls through. The block matcherblockmatch_end_else(38:4130) is what bounds these bodies during skips (e.g.While 0skips straight toEnd).- Dispatch path (byte-traced). The For/While/Repeat/End/Return execution handlers live
off page 0x38 — page 0x38 only has the
tFor/tWhile/tRepeat/tEndcompares inside theblockmatch_end_elseskip scanner (38:4130…4180). The live handlers are reached via the page-0x02 command dispatcher:02:54BDloads a per-token handler pointer (LD HL,0x6A30fortFor=CP 0xD3,0x6A34fortEnd=CP 0xD4,0x6A2AfortReturn=CP 0xD5), andtWhile/tRepeatload a loop-type code (LD A,0x26/0x27) andJP 0x6400.02:6400and the6A2A/6A30/6A34stubs set a command index (0x28/0x29/0x2A) and invoke bcall0x5140/0x513D, which both resolve to page 0x33 (_grf_435f, target33:435F).33:435FdoesSUB 0x20, bounds-checks, and indexes a 13-entry jump table at33:4381(0x47BB, 0x4A71, 0x4817, 0x4759, 0x47F5, 0x4AAA, 0x4B36, 0x4B4B, 0x45DE, 0x45D1, 0x459B, 0x4C93, 0x4CE8) — the actual For/While/Repeat/End/Return bodies. The defaultForstep uses_OP2Set1and the loop variable is stored via_MovFrOP1;Endre-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_DeallocFPS1thenrestore_982c_ctx(38:58DF, which setspTempCnt/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 falseIf: the implicit-close path at02:5676interacts with the false-Ifskip path and repeatedly advances temporary parser storage. See TI-BASICFor(optional paren trap.
Goto / Lbl: goto_lbl_name_scanner (38:4870) + scanner at 38:7600 [confirmed]
Lbl/Gotouse a name scanner.goto_lbl_name_scanner(38:4870) reads the label name aftertGoto(D7)/tLbl(D6): it advances over the (possibly 2-byte) label token(s) until EOL ('?'=0x3F) / end, records the position inparsePtr, then does across_page_jump(0x14)to the search routine. Token compares fortLbl(D6)at38:4870and38:7626;tGoto(D7)at38:762A. [confirmed]Gotoresolves by rescanning the program body from the top for a matchingLbl name, then settingparsePtrthere — the classic TI-BASIC behavior that makesGotoO(program size) and makesGotoout 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:Returnexits the current BASIC program and resumes the caller, whileStopexits the whole BASIC program chain back to the homescreen context.CALLSUB/SUBRTandCALLSTOP/STOPSUBare 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:
| Command | Token | Handler site(s) | Display primitive used |
|---|---|---|---|
Disp | tDisp=DE | dispatch → _Disp (37:51D3), bcall site 38:45A4 | _Disp, _NewLine, _DispDone |
Output( | tOutput=E0 | 38:6AE6 (CP E0), 02:673E, 01:7D3D | _OutputExpr (03:4AF2) at row,col |
Input | tInput=DC | 02:54EF, 02:56AB, 02:5917, 01:7DEF | prompt + entry-line editor + _ParseInp of typed text |
Prompt | tPrompt=DD | 02:562F, 02:5786, 02:590E, 00:4C5C | like Input but auto-labels NAME=? |
Menu( | tMenu=E6 | 38:5A8A (CP E6), 02:555D, 06:4A17 | _DispMenuTitle (39:4D21) + branch on choice |
Pause | tPause=D8 | 02:55E7, 02:6684, 39:6B8E, 3A:7E7C | display then wait for [ENTER] via key loop |
getKey | tGetKey=AD | 37:6700 (a token-attribute table, not a keymap), 3A:7E8A | non-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; otherwiseRST5(bjump) to the generic display path. Numeric results format via_DispOP1A(04:7844) →_CkOP1Real; strings/lists route through their formatters. EachDispitem ends with_NewLine(01:5F4A):curCol=0, and ifcurRow+1 >= winBtmit triggers scroll, elsecurRow++._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 (?forInput,VAR=forPrompt), 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):Inputdispatch is02:54EF(CP 0xDC) and the body entry02:54F6reached via02: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 +_ParseInpthe typed text; (6)_MovFrOP1/store into the target. With no args at all,Inputpauses on the graph screen with a free-moving cursor.Promptdispatch is02: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 0x1DF3then cross-pageCALL 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 at02:555D(CP 0xE6, → handler pointerLD 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-targetLbl, draws the option rows, blocks for a key, and on selection performs aGoto-style jump to the chosenLbl. Token site also38: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 at02: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(bcall0x4972→ page 0606:491E); the per-key numeric codes returned are the standard TIkXxxconstants (e.g.kRight=1,kLeft=2,kUp=3,kDown=4,kEnter=5,kClear=9,k0..k9 = 0x8E…).37:6700is 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 beginsCP 0xAD(tGetKey) /CP 0x55/CP 0x54and continues as records keyed by token (FE xx1-byte,FB xx/FC xx/F4 892-byte tokens — getKey, stat/distribution and finance tokens), used by a (de)tokenizer/compiler rather than as a key→code map. The keycodes agetKeyreturns come from_GetKeyon page 06, not this table. [confirmed: 37:6700 is a token-descriptor table; keycodes come from_GetKeyon 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]
parse_cur_tokfetches a token atparsePtr.chk_tok_end(38:72E0) classifies it into a small class number (<=3operand/expr,4= syntax error, others = operator/command). Flagged tokens reclassify viaset_split_rows(ram:20A0) whenIY+9 & 0x80.parse_cur_err_illegal(38:70F8) validates the current token; its caller (at38:6FBE) then maps the token byte to a grammar/precedence class — tokens≥0xF2get+0x12(38:6FBE: ADD A,0x12), folding the high token page into the class space.- 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^/!), or0x7175(leaf) —0x478Cand0x7175are raw code targets insideparse_eval_expr(not defined functions in the live DB), whereas0x4000is the pointer table itself. Nesting these realizes precedence. - Binary ops fold operands via FP RSTs (RST 30h
_FPAdd;_FPMult=00:238B, …) /_BinOPExec, leaving the result inOP1. - Variable tokens become an OP1 name (type byte + name) and resolve via
_FindSym/_RclVarSym(doc 05); store targets (→VAR) resolve through the38:7600name scanner (handles[A]/L1/Str1/Y-var/Ansclasses,_JError(0x8F)on an attempt to store intoAns). - Statement separators (
:and EOL0x3F) 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:6910 → 38:6914 → 38: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 L1–L4, 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/Menuargument order (§5).Input(02:54EF→54F6): optional leading prompt-string or(row,col)→ single store var → editor → parse → store.Prompt(02:562F→6699): 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/Enddispatch (§4). Execution handlers live on page 0x33 (jump table33:4381, entered via bcall0x5140/0x513D=33:435Ffrom the page-0x02 dispatcher at02:54BD/02:6400), not page 0x38.Endre-seeds the parse cursor from the loop record’s saved top position.getKey37:6700(§5). A fixed-width token-attribute / opcode-template table keyed by token (FE/FB/FC/F4-prefixed). The keycodes agetKeyreturns come from the OS_GetKeysystem call (bcall0x4972, page 06), which returns the standardkXxxconstants.
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 the33:4381handlers is confirmed. - The page-0x02
Input/Promptentry-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
- 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. TheFor(optional-paren trap is the sharpest example: with a first-line falseIf, dropping the closing)made anN=100benchmark grow from 521,723 to 885,912 marker-to-marker instructions. [confirmed] - Prefer built-ins for list-wide work.
SortA(,cumSum(, andsum(cross into OS routines that run one parser setup and then loop internally. TheDATA.8xptrace hitslist_fold_dispatch(02:6104) forsum(rather than reparsing an explicit BASIC accumulator loop for every element. [confirmed] - 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
TIFloatthrough OP registers. RepeatedL1(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] - Avoid
Gotoin hot loops.Gotosearches for a matchingLblby scanning the program token stream, and escaping structured loops throughGotocan leave loop bookkeeping behind. UseFor(/While/RepeatplusEndunless the jump is truly cold. [standard; scanner confirmed insub-tibasic.md] - Batch display and graph output.
DispandOutput(reach display primitives and LCD update paths; graph drawing reaches graph-buffer and pixel routines before display copy. Draw into the graph buffer and callDispGraphonce when possible. [confirmed] - 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-Ifinteraction. [confirmed]
Trace-backed cost map
| Pattern | Trace evidence | Practical rule |
|---|---|---|
Straight-line display (HELLO) | page-38 statement parse plus _Disp | Fine for status text; avoid using Disp as a frame loop. |
Prompted arithmetic (FACTOR) | loop-body reseed, FP multiply, display | Keep loop bodies short; store loop-invariant values before For(. |
List built-ins (DATA) | sum( reaches list_fold_dispatch | Prefer built-ins when one parser setup can cover many elements. |
Text animation (ANIMTXT) | Output( plus LCD text paths on every loop | Precompute positions/strings and update the smallest region possible. |
Graph drawing (GRAPHV) | primitives draw into plotSScreen, then _PDspGrph | Batch graph primitives before DispGraph. |
Graph visualization (GRAPHDFS, GRAPHLST) | window stores plus repeated Line(/Circle(/Text( reach _StoSysTok, _ILine, _IPoint, graph_pixel_op, _PDspGrph, and small-font paths; GRAPHLST also reaches list indexing in draw arguments | Store graph topology in lists; draw the whole view in one graph-buffer pass. |
BASIC subprogram (CALLSUB, CALLABI) | page-38 program-body evaluator and shared VAT variables | Treat globals/lists/Ans as the calling convention. |
List algorithms (BIGADD, BIGMUL, DFS) | VAT lookup, element address, OP-register move per access | Preallocate lists; cache dimensions and reused elements in scalars. |
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 area | Fixture or probe | Evidence |
|---|---|---|
| Hello world | HELLO.8xp | Displays HELLO, WORLD, then Done; reaches page-38 statement parsing and _Disp. |
| Factorial | FACTOR.8xp | Prompt input 5 displays 120; reaches loop parsing and _FPMult. |
| Data manipulation | DATA.8xp | Sorts, cumulatively sums, and displays list data; reaches list element stores and sum(’s list fold path. |
| Text animation | ANIMTXT.8xp | Moves/writes X characters with Output(, then displays DONE; reaches LCD text routines each loop. |
| Graph drawing | GRAPHV.8xp | Renders DFS, axes, a circle, and diagonal line on the graph screen; reaches _ILine, _IPoint, and _PDspGrph. |
| Graph visualization | GRAPHDFS.8xp, GRAPHLST.8xp | Renders 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 arithmetic | BIGADD.8xp, BIGMUL.8xp | Adds and multiplies digit lists with carry propagation; reaches list indexing and FP helper paths. |
| DFS / stack-style list algorithm | DFS.8xp | Displays traversal 1, 3, 2, 4 and visited list {1 1 1 1}; reaches nested scanner/control-flow paths. |
| BASIC subprogram calling convention | CALLSUB.8xp + SUBRT.8xp; ABICALL.8xp + ABISUB.8xp; CALLSTOP.8xp + STOPSUB.8xp | Caller and callee share scalar/list/Ans state; Return resumes the caller, while Stop terminates the caller chain. |
| BASIC to ASM | ASMCALL.8xp + ASMRET.8xp | Asm( runs an AsmPrgm payload (C9) and returns to BASIC, displaying BEFORE then AFTER. |
| ASM-directed BASIC callback | ASMBRIDG.8xp + ASMSIG.8xp + ZZBASIC.8xp | ASM sets Ans=1 with _OP1Set1/_StoAns, returns, and BASIC calls prgmZZBASIC through If Ans. |
| ASM return value | ASMRTN.8xp + ASMVAL.8xp | ASM sets Ans=2 with _OP1Set2/_StoAns; BASIC reads Ans, computes Ans+3, and displays 5. |
| ASM-side BASIC lookup | ASMFIND.8xp + ZZFIND.8xp + ZZBASIC.8xp | AsmPrgm can build OP1={ProgObj,"ZZBASIC"} and reach findsym_scan, then return to BASIC without running ZZBASIC. |
| Direct ASM to BASIC execution | ASMPARSE.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:
| Node | DFS value | Pixel center | Label position |
|---|---|---|---|
| 1 | root | (10,44) | Text(16,8,"1") |
| 2 | first edge target | (35,54) | Text(6,33,"2") |
| 3 | second edge target | (35,14) | Text(46,33,"3") |
| 4 | child of 2 | (55,29) | Text(31,53,"4") |
The edge lists L1={1,1,2} and L2={2,3,4} become the three line segments
1-2, 1-3, and 2-4. The fixture stores window variables first so these
pixel-like coordinates cover the visible graph area.
Observed run: the final graph screen shows four labeled nodes with edges
1-2, 1-3, and 2-4. The trace hits _ILine (04:4029),
graph_pixel_op, _IPoint, _PDspGrph (04:7904), small-font glyph
rendering, window variable stores through _StoSysTok, _RestoreDisp, and
page-38 statement evaluation. [confirmed]
The performance lesson is to separate graph data from graph drawing. Keep edge lists and traversal state in lists, but convert them to pixels in a single draw phase instead of interleaving traversal, display, and recalculation.
GRAPHLST.8xp makes that separation explicit. It stores edge endpoint
coordinates in L1–L4 and node centers in L5/L6, then draws edges and
nodes with loops:
{10,10,35}->L1
{44,44,54}->L2
{35,35,55}->L3
{54,14,29}->L4
{10,35,35,55}->L5
{44,54,14,29}->L6
For(I,1,3)
Line(L1(I),L2(I),L3(I),L4(I))
End
For(I,1,4)
Circle(L5(I),L6(I),3)
End
Observed run: GRAPHLST.8xp renders the same four-node topology as
GRAPHDFS.8xp; the smoke runner checks the same node and edge crop regions.
The trace additionally hits list_var_index and _GetLToOP1, proving that the
draw arguments came through list element recall rather than hard-coded
coordinates. [confirmed]
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:6910 → 38:6914 → 38:778F
sequence, reached after parser RAM has already been populated:
| RAM state | Address | Role in the private parser frame |
|---|---|---|
basic_prog | 9652 | current OP1-style program/object name |
basic_start | 965B | first token byte after the stored program size word |
nextParseByte | 965D | current parser cursor |
basic_end | 965F | parser end pointer |
numArguments | 9661 | argument count/state byte used by parser helpers |
chkDelPtr3 / chkDelPtr4 | 981C / 981E | temporary VAT/data pointers used during name and object setup |
FPS / OPS / pTemp / progPtr | 9824 / 9828 / 982E / 9830 | live FP/temp/program storage bounds |
There is no local variable frame for BASIC programs. A subprogram that uses A
modifies the caller’s A. For reusable routines, document which variables are
inputs, scratch, and outputs.
| ABI part | Practical convention | Trace evidence |
|---|---|---|
| Inputs | Scalars, lists, and Ans are shared across caller and callee. The caller stores them before prgmNAME. | CALLSUB stores A; ABICALL seeds L1 and Ans. |
| Outputs | The callee stores results back to globals, list elements, or Ans. | SUBRT increments shared A; ABISUB writes A, L1(3), and Ans. |
| Scratch | No automatic save/restore exists. Routines must document scratch variables. | The VAT and parser state are shared across caller and callee. |
| Return/Stop | Return exits the callee and resumes the caller. Stop terminates the whole program chain. | SUBRT returns to CALLSUB, which then runs Disp A; STOPSUB stops CALLSTOP before caller text AFTER can display. |
| Parser state | prgmNAME runs with private parser/FPS state already set up by BASIC. | The callee path reaches 38:6910 → 38:6914 → 38:778F. |
ABICALL.8xp broadens that scalar-only case:
{2,4,6}->L1
7
prgmABISUB
Disp A
Disp L1
Disp Ans
with callee:
Ans+L1(2)->A
9->L1(3)
A
Return
Observed run: ABICALL.8xp and ABISUB.8xp display 11, {2 4 9}, 11,
then Done. The callee reads the caller’s Ans=7 and L1(2)=4, stores 11
in shared scalar A, mutates shared L1(3) to 9, evaluates A as the final
callee expression so Ans is also 11, and returns. The smoke runner checks
the rendered scalar, list, Ans, and Done regions, and the trace hits
stmt_eval_body_entry, call_eval_eqn_recursive, eval_eqn_recursive,
_AnsName, and store_list_elem. [confirmed]
CALLSTOP.8xp and STOPSUB.8xp cover the non-returning branch:
Disp "BEFORE"
prgmSTOPSUB
Disp "AFTER"
with callee:
Disp "STOP"
Stop
Observed run: CALLSTOP.8xp and STOPSUB.8xp display BEFORE, then STOP,
then Done; AFTER never appears. The smoke runner checks the BEFORE,
STOP, and Done regions and also checks a low-pixel region where AFTER
would be drawn if the caller resumed. The trace reaches stmt_eval_body_entry,
call_eval_eqn_recursive, and _Disp. This confirms that Stop in a callee
terminates the whole BASIC program chain instead of returning to the caller.
[confirmed]
Arbitrary-precision decimal addition
BIGADD.8xp uses lists of base-10 digits in little-endian order. 12345 is
{5,4,3,2,1}, 98765 is {5,6,7,8,9}, and the result is the list
{0,1,1,1,1,1} for 111110.
{5,4,3,2,1}->L1
{5,6,7,8,9}->L2
{0,0,0,0,0,0}->L3
0->C
For(I,1,5)
L1(I)+L2(I)+C->S
int(S/10)->C
S-10C->L3(I)
End
C->L3(6)
Disp L3
Disp L3(6)
Observed run: the list line begins {0 1 1 1 1 ...}, the explicit carry line is
1, and the program ends with Done. The trace hits list element address and
store paths (list_var_index, _AdrLEle, _GetLToOP1, _PutToL,
store_list_elem*) plus fnint_body, _FPDiv, _FPAdd, _FPSub, and
_FPMult. [confirmed]
Performance notes: this is intentionally simple, but it is parser-heavy. For a
general routine, cache dim(L1) and dim(L2) before the loop, avoid repeated
list indexing when a digit is reused, and use a larger base only if you can
tolerate more carry and display conversion work.
For a reusable arbitrary-precision add routine, treat L1 and L2 as
little-endian digit arrays and compute the loop bound from list lengths:
dim(L1)->N
If dim(L2)>N
dim(L2)->N
0->C
For(I,1,N)
0->A
0->B
If I<=dim(L1)
L1(I)->A
If I<=dim(L2)
L2(I)->B
A+B+C->S
int(S/10)->C
S-10C->L3(I)
End
If C
C->L3(N+1)
The invariant after iteration I is that L3(1..I) contains the low I
digits of L1+L2, and C is the carry into digit I+1. Base 10 is easy to
display and debug. A larger base reduces loop count but adds conversion and
larger carry values; on TI-BASIC, that tradeoff only helps when display is not
part of the hot path.
Arbitrary-precision decimal multiplication
BIGMUL.8xp uses the same little-endian digit convention for schoolbook
multiplication. The example multiplies 123 ({3,2,1}) by 45 ({5,4}), so
the expected result is 5535, represented as {5,3,5,5,0}.
{3,2,1}->L1
{5,4}->L2
{0,0,0,0,0}->L3
For(I,1,3)
For(J,1,2)
L3(I+J-1)+L1(I)*L2(J)->S
int(S/10)->C
S-10C->L3(I+J-1)
L3(I+J)+C->L3(I+J)
End
End
Disp L3
Disp L3(4)
Observed run: BIGMUL.8xp displays {5 3 5 5 0}, then 5, then Done.
The trace hits nested For( loop parsing, list element reads/stores, _FPMult,
_FPAdd, _FPSub, _GetLToOP1, and _PutToL. [confirmed]
The invariant is that each inner-loop step normalizes one result cell
L3(I+J-1) and carries into the next cell. This is still base-10 arithmetic,
so it favors trace readability over speed. A larger base reduces the number of
digits but makes the carry path and display conversion heavier.
DFS with a list stack
DFS.8xp uses two edge lists (L1 source, L2 destination), a visited list
(L3), and an explicit stack (L4) to traverse this graph:
1 -> 2
1 -> 3
2 -> 4
{1,1,2}->L1
{2,3,4}->L2
{0,0,0,0}->L3
{1,0,0,0}->L4
1->P
While P
L4(P)->V
P-1->P
If L3(V)=0
Then
1->L3(V)
Disp V
For(E,1,3)
If L1(E)=V
Then
P+1->P
L2(E)->L4(P)
End
End
End
End
Disp L3
Observed run: traversal order is 1, 3, 2, 4 because the stack is LIFO and
node 3 is pushed after node 2. The final visited list is {1 1 1 1}. The
trace hits blockmatch_end_else, parse_scan_tokens, eval_stmt_entry,
parser refill/advance paths, _Disp, and the same list read/write helpers used
by BIGADD. [confirmed]
Performance notes: this version scans all edges for every visited node, so it is
easy to understand but O(VE) in BASIC-level work. For larger graphs, keep an
offset table of edge ranges per node, avoid augment( in hot loops, and
preallocate stack/visited lists with scalar pointers as this sample does.
The loop maintains three invariants:
L3(V)=1means nodeVhas already been displayed and expanded.L4(1..P)is the pending stack, withL4(P)popped next.- Edges are scanned from left to right, so pushing node
2and then node3makes node3display before node2.
The trace cost follows those invariants. Every While and nested If Then
forces the interpreter to scan for block boundaries (blockmatch_end_else,
parse_scan_tokens), and every L1(E)/L2(E) access goes through VAT lookup
and list-element address calculation. Precomputed adjacency ranges reduce both
the number of edge scans and the number of interpreted branch scans.
BASIC and ASM interop
BASIC to ASM
The validated smoke test is:
Asm(prgmASMRET)
with:
AsmPrgm
C9
Asm( is token BB 6A; AsmPrgm is BB 6C; prgm is token 5F. The
Asm( command handler parses the following prgmNAME token stream, then
bcalls _ExecutePrgm (4E7C, target 07:5758). The trace shows that path
compile/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:6910 → 38:6914 → 38:778F). [confirmed]
This is a callback convention, not a direct jump from ASM into a BASIC body.
The ASM side communicates a return code through Ans; BASIC owns the parser
state, performs the prgm call, and resumes after the target returns.
For a numeric return value without a BASIC callback, ASMVAL.8xp stores 2 in
Ans:
RST 28h
.dw 41A7h ; _OP1Set2
RST 28h
.dw 4ABFh ; _StoAns
RET
The wrapper consumes it as an ordinary BASIC value:
Asm(prgmASMVAL)
Ans+3->A
Disp A
Observed run: ASMRTN.8xp and ASMVAL.8xp display 5, then Done. The trace
hits 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]
| Direction | Confirmed mechanism | Caveat |
|---|---|---|
| BASIC → ASM | Asm(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 → BASIC | prgmNAME enters the page-38 parser/VAT/body evaluator path and Return resumes the caller. | There is no local frame; variables, lists, and Ans are shared. |
| ASM → BASIC callback | ASM stores a signal/result such as Ans=1, returns, and the BASIC wrapper conditionally runs prgmNAME. | BASIC must own the actual prgm call; this is cooperative, not an arbitrary ASM bcall into BASIC. |
| ASM → BASIC value return | ASM stores a numeric result in Ans with _StoAns; BASIC resumes and evaluates Ans. | This returns data to BASIC, not control into a BASIC program body. |
| ASM → VAT lookup | ASMFIND builds OP1={ProgObj,"ZZBASIC"} and bcalls _ChkFindSym. | Lookup is not execution; the wrapper returns and ZZBASIC does not display CALLED. |
| Direct ASM → BASIC | No working public bcall sequence is proven in this repo. | ASMPARSE reaches _ParseInpLastEnt/_ParseInp and then ERR:INVALID; ASMFORM reaches _Find_Parse_Formula and then ERR:UNDEFINED; 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:
_ExecutePrgmis theAsmPrgmexecutor reached byAsm(prgmNAME), not a general “run a BASIC program” entry._ExecuteNewPrgm(4C3C, target00:265F) is not a drop-in BASIC runner from an arbitraryAsmPrgmeither. It expects more OS state than just a name pointer._ParsePrgmName(4E82, target38:40D4) only consumes aprgmNAMEtoken from the current parser cursor and builds the name object used byAsm(.
The confirmed BASIC subprogram path is different: the CALLSUB/SUBRT trace
does not hit _ParsePrgmName, _ExecutePrgm, _Find_Parse_Formula, or
_SetParseVarProg. It resolves the program name through the page-38
parser/VAT path, enters the program-body evaluator at 38:6914 →
38:778F, and lets Return unwind to the caller. Calling that same machinery
from arbitrary ASM requires more than loading OP1 and bcalling a single public
entry; it needs the same parser cursor, stack, error, and run-state setup that a
live BASIC caller already has. [hypothesis]
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( → _ExecutePrgm → ram:9D95, and BASIC prgmNAME →
38: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
| Case | Program(s) | Operations exercised | Anchor examples |
|---|---|---|---|
hello | HELLO.8xp | ClrHome, Disp, string scan, Done | eval_stmt_entry, _Disp |
factorial | FACTOR.8xp | Prompt, scalar stores, For(/End, FP multiply | _FPMult, _Disp |
data | DATA.8xp | list literal, SortA(, cumSum(, sum( | store_list_elem, list_fold_dispatch |
asmcall | ASMCALL.8xp + ASMRET.8xp | BASIC Asm(prgmNAME) into AsmPrgm payload | _ExecutePrgm, ram:9D95 |
asmbridge | ASMBRIDG.8xp + ASMSIG.8xp + ZZBASIC.8xp | ASM return code through Ans, BASIC callback | _OP1Set1, _StoAns, _AnsName, eval_eqn_recursive |
asmreturn | ASMRTN.8xp + ASMVAL.8xp | ASM return value through Ans, then BASIC arithmetic | _OP1Set2, _StoAns, _AnsName, _FPAdd |
asmfind | ASMFIND.8xp + ZZFIND.8xp + ZZBASIC.8xp | ASM-side VAT lookup of a BASIC program without executing it | ram:9D95, findsym_scan, _Disp |
asmparse | ASMPARSE.8xp + ZZPARSE.8xp + ZZBASIC.8xp | ASM parser-entry negative probe ending at ERR:INVALID | _ParseInpLastEnt, _ParseInp, parseinp_find_setup |
asmformula | ASMFORM.8xp + ZZFORM.8xp + ZZBASIC.8xp | ASM formula-parser negative probe ending at ERR:UNDEFINED | _Find_Parse_Formula, parse_init_findsym, findsym_scan |
animtext | ANIMTXT.8xp | text placement animation with Output( | _OutputExpr, _Disp |
graphviz | GRAPHV.8xp | graph-buffer primitives and DispGraph | _GrBufClr, _ILine, _IPoint, _PDspGrph |
graphdfs | GRAPHDFS.8xp | graph visualization from DFS topology | _StoSysTok, _ILine, _IPoint, _PDspGrph |
graphlist | GRAPHLST.8xp | list-driven graph visualization from edge/node coordinate lists | list_var_index, _GetLToOP1, _ILine, _IPoint |
callsub | CALLSUB.8xp + SUBRT.8xp | BASIC prgmNAME, shared globals, Return | stmt_eval_body_entry, call_eval_eqn_recursive |
callabi | ABICALL.8xp + ABISUB.8xp | BASIC subprogram ABI through Ans, scalar A, and list L1 | _AnsName, store_list_elem, eval_eqn_recursive |
callstop | CALLSTOP.8xp + STOPSUB.8xp | BASIC subprogram Stop terminates the caller chain | stmt_eval_body_entry, call_eval_eqn_recursive, _Disp |
bigadd | BIGADD.8xp | list-digit arithmetic and carry propagation | list_var_index, _GetLToOP1, _PutToL, _FPMult |
bigmul | BIGMUL.8xp | list-digit multiplication, nested loops, carry normalization | list_var_index, _GetLToOP1, _PutToL, _FPMult |
dfs | DFS.8xp | list-backed stack, nested While/If/For | blockmatch_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:6910 → 38:6914 → 38: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.
Related pages
- TI-BASIC programs explains the parser, statement evaluator,
control flow, display commands,
Asm(, andprgmNAME. - TI-BASIC programming patterns turns the traces into performance and calling-convention guidance.
- TI-BASIC
For(optional paren trap is a focused trace study of one parser-performance edge case.
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 body | N | For(... ) | For(... | Delta |
|---|---|---|---|---|
If 0 / 1 | 25 | 144,805 instr / 1,519,710 clocks | 156,292 instr / 1,604,282 clocks | +7.9% instr / +5.6% clocks |
If 0 / 1 | 100 | 521,723 instr / 5,498,347 clocks | 885,912 instr / 8,862,729 clocks | +69.8% instr / +61.2% clocks |
If 1 / 1 | 25 | 185,032 instr / 1,920,085 clocks | 179,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]:
For(with explicit)enters the02:56C3close/cleanup path before the control-flow handler records the loop continuation.For(without)takes theC=0direct path at02:5687.- With a first body line of
If 0, the loop immediately enters the single-statement false-Ifskip path (if_isg_stmt_handlerat38:6F63, with skip/temporary-parser work through the page-38 statement evaluator). - 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) loadsA=0xB8, subtracts8each pass and stops at0x80→ row commands0xB8 … 0x80(8 rows of 8 px = 64 px tall), calling_ClearRowper row with interrupts masked (DI). [confirmed] - Command bytes (all grounded in
_ClrLCDFull/_ClearRow/lcd_set_col_cmd):- Row (page) select =
0xB8 − 8·rowforrow = 0…7(i.e.0xB8, 0xB0, … 0x80), sent to0x10vialcd_set_col_cmd(01:5A89), which only emits the byte when0x80 ≤ A < 0xC0(guards the row/Z-address range)._ClrLCDFullwalks this by loadingA=0xB8, calling_ClearRow, thenSUB 0x8and looping whileA ≥ 0x80(B=0x80). - Column select =
0x20 + col, sent raw to0x10._ClearRow(01:6934) walksEfrom0x20to0x2B(CP 0x2Cterminates) = 12 columns (12 bytes × 8 px = 96 px wide), writing 8 data bytes to0x11per column — theB=0x08innerdjnzloop writes one byte per pixel row (8 rows). [confirmed] - Contrast:
lcd_set_contrast(01:5A59) writes the contrast level to the data port0x11;lcd_get_contrast(01:5A60) reads it back from the controller withIN A,(0x11)(the standard dummy+real LCD read), not a command re-send. The level is also held in RAM atcontrast(0x8447); the command-port form(contrast+0x18)|0xC0is what_LCD_DRIVERON(page 06) and the_GetKeycontrast keys send to0x10. [confirmed] - Every port access is preceded by
CALL ram:0CC3(lcd_wait), the controller-busy delay. [confirmed]
- Row (page) select =
Text output [confirmed]
curRow/curCol(0x844B/844C) — the homescreen text cursor (16 columns wide;_PutCwraps at col 16, calls_NewLine)._PutMap(01:5A98) draws one large-font character at the cursor: it clamps invalid codes to0xD0, computes an initialchar * 8offset, then bjumps to the page-7 large-font blitter, which adjusts that offset to the actual7-byte-strideglyph table before copying an 8-byte render record. [confirmed]_PutC(01:5B4C) =_PutMap+ advance cursor + newline handling;_PutSprints a string;_NewLinescrolls._DispHL(01:5BF6) printsHLas a right-justified 5-digit decimal: repeated_DivHLBy10, digits +0x30, leading zeros → spaces, writing the digits backward from0x847Cinto theOP1scratch area, then_PutC/_PutMapeach 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) blitsplotSScreento 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 a7-byte strideper glyph (not 8)._PutMap(01:5A98) clamps the code (0or≥0xF8→0xD0), computesHL = char*8(threeADD HL,HL), then bjumps via trampolineram:3B3Dto the blitterput_glyph_large(07:4588). The blitter doesHL = 07:45FF + char*8, thenlgfont_glyph_ptr_adjust(07:45EB) subtractschar(it shiftschar*8right by 3 →char, thenSBC HL,DE), yielding the real glyph pointer07:45FF + char*7. It then copies an 8-byte record via_Mov8B(ram:1A94, 8×LDI) into RAM at0x845A(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 at07:45FF + char*7.) - Alternate large fonts: two bits in
(IY+0x35)select a replacement glyph source before the page-7 table read — bit 5 loadsA=0x01and callsram:36E7(bjump to3B:7BFB), bit 1 loadsA=0x76and callsram:3E1F(bjump to3B:7B9C); both are font-hook routines on page3Bthat take theAvalue as a selector. When neither bit is set, the page-7 table at07:45FFis used. [confirmed] - Small/variable-width font:
_VPutMap/_VPutS(graph screen, pixel-addressed viapenCol/penRow).
Indicators
flags.indicFlagsbit 0 = the run/busy indicator (the moving dashes top-right);_ClrLCDFullpreserves it across a clear;_RunIndicOn/_RunIndicOfftoggle it. [confirmed]
LCD command bytes and glyph table
- LCD command bytes confirmed by tracing
_ClrLCDFull(01:60E4),_ClearRow(01:6934) andlcd_set_col_cmd(01:5A89): row (page) select =0xB8 − 8·row(range0xB8 … 0x80, stepping down by 8), column select =0x20 + col(range0x20 … 0x2B, 12 columns = 96 px), command port0x10/ data port0x11, busy-wait viaram:0CC3. Contrast is held at RAMcontrast(0x8447) and written to the data port0x11bylcd_set_contrast(01:5A59). See Controller. [confirmed] - Large-font glyph table pinned: page
0x07, base07:45FF,7-byte stride(put_glyph_large@07:4588→ glyph ptr07:45FF + char*7via07:45EB, then_Mov8Bcopies an 8-byte record to RAM0x845A). 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.
| Addr | Name | Meaning |
|---|---|---|
0x8F50 | Xmin | left edge real X |
0x8F59 | Xmax | right edge real X |
0x8F62 | Xscl | X tick spacing |
0x8F6B | Ymin | bottom edge real Y |
0x8F74 | Ymax | top edge real Y |
0x8F7D | Yscl | Y tick spacing |
0x8F86 | ThetaMin / 0x8F8F ThetaMax / 0x8F98 ThetaStep | polar/parametric range |
0x900D | XresO | Xres (pixel step between plotted columns) |
0x9151 | Xres_int | integer copy of Xres |
0x9152 | deltaX | (Xmax−Xmin)/94 — real width of one pixel column |
0x915B | deltaY | (Ymax−Ymin)/62 — real height of one pixel row |
0x9164 | shortX | scratch/divisor float for the X transform (per-pixel ΔX) |
0x916D | shortY | scratch/divisor float for the Y transform (per-pixel ΔY) |
0x913F | XFact / 0x9148 YFact | ZOOM IN/OUT factors |
There is a second “u” copy block at 0x8E7E (uXmin…uXres at 0x8F3B) — the
uVar window set used in the alternate (split/table) graph context, and a working/temp
float pair around 0x8E6A/0x8E73 used by the transform code. [confirmed]
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 20hpushes/loads OP1 (the input value),CALL 228Fmoves theminoperand in and subtracts it (value − min),CALL 2385divides by the per-pixel delta (shortX/shortY),- the X path additionally adds the
0x8E73X-origin term, the Y path negates so that larger Y maps to a smaller row (screen Y grows downward), CALL 4229clamps/handles the float→integer exponent (readsOP1.expat0x8479, bias0x7F) and rounds to an integer pixel; out-of-range loads ±large sentinel._YftoIreturns 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 1BA7zeroes the destination mantissa,CALL 5F6Aconverts the binary value to packed BCD by repeatedADD A,0x16 / DAA(binary→decimal nibble accumulation), looping A times,- the exponent byte is set so OP1 holds the integer;
_SetXXXXOP2(33:5F9E) is the 4-digit (up to 9999) variant for larger pixel/coordinate counts.
These are used to turn a pixel column/row (e.g. under the TRACE cursor) back into the real X/Y shown at the bottom of the screen, and by DRAW commands that take pixel arguments.
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:
| bcall | Addr | Command |
|---|---|---|
_HorizCmd | 04:793E | Horizontal y — draws a full-width horizontal line at real Y. See note below. |
_VertCmd | 04:7955 | Vertical x — draws a full-height vertical line at real X. See note below. |
_LineCmd | 04:796A | Line(x1,y1,x2,y2) — _PDspGrph, optionally draws via page 33, then JP 0x152A = _DeallocFPS1(0x24) frees the coord frame (the alloc happens upstream). |
_UnLineCmd | 04:797C | Line(…,0) — erase variant (same path, clear mode). |
_PointCmd | 04:79B2 | Pt-On/Pt-Off/Pt-Change( — reads style from OP1.mantissa[0] & 0x20, dispatches set/clear/toggle. |
_DrawCmd | 04:7B8B | top-level DRAW dispatch — grabs the pending count and cross-jumps to the per-command handler. |
_DrawZeroOP1 | 04:620B | seeds OP3=0 then draws (used for axis / DrawF zero baseline). |
Note: _HorizCmd/_VertCmd both CALL 7933 first, which allocates a 0x24-byte FPS frame
(LD HL,0x24 / CALL 1537 / SBC HL,DE) and returns a pointer to it. _HorizCmd then builds the
line’s two endpoints in that frame: it copies Xmin (0x8F50) and Xmax (0x8F59) — the window’s
X range — with _Mov9B (00:1A92, which reads a window float into the frame), interleaving the
line’s Y (OP1) via _MovFrOP1 (00:1B0C), so the endpoints are (Xmin, y) and (Xmax, y).
_VertCmd does the same with Ymin (0x8F6B)/Ymax (0x8F74) and the line’s X. It renders with
_PDspGrph, then _DeallocFPS1(0x24) frees the frame — the window variables are read only,
so the line just spans the current window edges. [confirmed]
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)&1is set (graphFlags.graphDraw, incgraphFlags=3/graphDraw=0;1=redraw needed — this is thegraphFlagsbit atIY+3, distinct fromgrfDBFlagsatIY+4and SmartGraph atIY+0x17), calls_Regraphto recompute the whole plot, - otherwise checks the split-screen flag (
_Bit_VertSplit) and copies the buffer to the LCD (graph_redraw_buf04:607F).
_GrBufCpy (04:60A3) blits plotSScreen to the LCD: handles split-screen
(_CheckSplitFlag, _Bit_VertSplit), draws the split divider line (_DarkLine/_ILine at
column region 0x2F), sets normal display vals, and walks the rows. [confirmed]
_RestoreDisp (04:6176) is the actual row-blit loop: for each of the up-to-64 rows it
issues the 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. Y1…Y0 (and r1…, X1T/Y1T,
u/v/w) are system equation vars. Each holds the tokens you typed after Y1=. The
equation’s flags byte is 0x23 when selected (plotted) and 0x03 when deselected, so
the selection bit is bit 5 (0x20). The per-equation style byte holds the line style:
0=line, 1=thick, 2=shade above, 3=shade below, 4=trace/path, 5=animate, 6=dotted
(curGStyle 0x8D17 is the current-equation copy). [confirmed — 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:
- compute the real
Xfor the column fromXmin + col*deltaX(the inverse of_XftoI), - store it into the
Xsystem variable, _ParseInp(38:5987) parses+evaluates the selected equation’s tokens against the currentX(it resets the parser state, clears a status bit at(IY+0x1F), and runs the formula evaluator_ChkFindSym/Find_Parse_Formula), leaving the resultYin OP1,_YftoImaps thatYto a pixel row,_ILineconnects this point to the previous column’s point (or_IPointfor dotted style), drawing intoplotSScreen.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/curColtext cursor (see 08-display-lcd.md). The graph screen is the pixel bufferplotSScreenrendered by the routines above; small-font labels (coords, TRACE readout) go through_VPutMap/penCol(0x86D7)/penRow(0x86D8). [confirmed 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/_SetXXOP2to convert the cursor pixel back to the real X/Y it prints at the bottom. [confirmed] - A
DRAWcommand (_DrawCmd) orLine(/Circle(/Pt-On(draws straight intoplotSScreenover the current plot and persists across a SmartGraph redraw (it is not re-evaluated) untilClrDrawis issued. [confirmed]
8. Confidence summary / open items
- Forward transform
(value−min)/pixelDelta: structure [confirmed] from the37:41F2disassembly (subtract228F, divide2385); the exact rounding in4229is read but the ±sentinel constants are summarized, not exhaustively byte-traced. _HorizCmd/_VertCmdendpoint build:7933allocates a 0x24-byte FPS frame, and the commands_Mov9Bthe window edges (Xmin/XmaxorYmin/Ymax) plus_MovFrOP1the 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_GrphCircsetup is confirmed. - Y= selection bit (
0x20; flags byte0x23selected /0x03deselected) and the style byte values (0–6) 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]
| Addr | Name | Meaning | Token |
|---|---|---|---|
0x92B3 | TblMin (a.k.a. TblStart) | first independent value in the table | tTblMin/TBLMINt = 0x1A |
0x92BC | TblStep (ΔTbl) | increment between successive rows | tTblStep/TBLSTEPt = 0x21 |
Both are 9-byte floats. They are ordinary system token variables: read/written
through _RclSysTok (38:683E) / _StoSysTok (38:623B) using the token bytes
above (the page-38 system-var token table lives around 38:61F1). ΔTbl’s token
is the list-step token 0x21; TblStart uses 0x1A. [confirmed token+addr]
Mode flags — tblFlags (IY+19 = IY+0x13) [confirmed bit layout]
From ti83plus.inc and verified by the bit-ops below:
| Bit | Name | Meaning |
|---|---|---|
4 (0x10) | autoFill | Indpnt: 0 = Auto (fill X from TblStart/ΔTbl), 1 = Ask (prompt the student for each X) |
5 (0x20) | autoCalc | Depend: 0 = Auto (compute Y immediately), 1 = Ask (compute a cell only on request) |
6 (0x40) | reTable | 0 = cached table valid, 1 = must recompute the table |
TBLSET key/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:
| Var | Token | Var | Token | |
|---|---|---|---|---|
Y1 | 0x10 | Y6 | 0x15 | |
Y2 | 0x11 | Y7 | 0x16 | |
Y3 | 0x12 | Y8 | 0x17 | |
Y4 | 0x13 | Y9 | 0x18 | |
Y5 | 0x14 | Y0 | 0x19 |
(Parametric X1T/Y1T=0x20/0x21…, polar r1…, and u/v/w sequences share the
same EquObj/tVarEqu machinery.) [confirmed 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):
| bcall | Addr | Role |
|---|---|---|
_GraphTblFind | 33:7097 | (re)build the list of selected-equation pointers |
_GraphTblNext | 33:707A | _LdHLind(0x84D9 + 2·n) — fetch the n-th equation pointer |
_grf_7066 | 33:7066 | store 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:
- store the running-X into the
Xsystem variable (_StoX,38:62A3), - 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 (_ParseInp38:5987/ the_Find_Parse_Formulapath), leavingYinOP1(exactly the grapher’s per-column eval insub-graphing.md §6), - 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):
| Addr | Role |
|---|---|
0x918C XOutSym / 0x918E XOutDat | X column: symbol + data pointer |
0x9190 YOutSym / 0x9192 YOutDat | active Y column: symbol + data pointer |
0x9194 inputSym / 0x9196 inputDat | the “Ask”/input column descriptor |
0x9198 prevData | previous-column data pointer |
0x91DB / 0x91DC / 0x91E0 | row 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 toX, 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:
| Site | Trigger |
|---|---|
02:7B35 bytes | editing TblStart/ΔTbl/Indpnt/Depend in TBLSET |
37:5F3D | toggling Indpnt or Depend on the setup screen |
38:6340, 38:4809, 38:54CD | the parser storing into a Y= equation or a relevant var (editing Y1=…, →Y1, or changing X/window) |
boot / reset (RAM clear) | initialises the table as dirty (reTable set); the exact init site is not pinned here (00:4105 is the “Resetting All…” message string, not the setter) |
Conversely only the recompute driver clears it (05:5DD7, 05:62FD,
05:64DE → RES 6,(IY+0x13)). [confirmed]
5. End-to-end: a student tabulates Y1=X² + 1
- Y=: types
X²+1afterY1=. The editor tokenizes it and stores the bytes as theEquObjY1(token5E 10) in the VAT, with its flags byte’s select bit set (the=is highlighted). The parser store path setsreTable. - TBLSET (
2nd WINDOW): setsTblStart=0(TblMin0x92B3),ΔTbl=1(TblStep0x92BC),Indpnt:Auto,Depend:Auto. Each edit setsreTable(the setup bytes around02:7B35). - TABLE (
2nd GRAPH): enters contextcxTableEditor(0x4A) on page 05.table_editor_main(05:5D0D) seesreTable=1→table_recompute(05:5DD7):- seed running-X ←
TblMin(05:774B), _GraphTblFind/_GraphTblNext(33:7097/707A) walk the selected equation list atiMathPtr4(0x84D9) — here onlyY1,- per row:
_StoXthe running-X, evaluateY1’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.
- seed running-X ←
- The grid paints (
05:7E45) the cachedXandY1columns as large-font text; scrolling (05:6014) slides the cache and computes only newly exposed rows. - Deselecting
Y1(or editing the formula, or changingΔTbl) setsreTableagain and the next view recomputes.
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
tblFlagsbit layout, and which sites set/clearreTable: [confirmed] (equates + byte-verified bit-ops). - Page 05 = TABLE subsystem, the recompute→clear-reTable structure, the running-X
seed from TblMin and
+TblStepadvance, the cell-cache buffers, the scroll (LDIR/LDDR), and the text-grid paint loop: [confirmed] from byte disassembly; the dense Z80 bodies don’t fully reduce in the decompiler but the CALL/buffer structure is byte-pinned. - The exact per-row
_StoX+ selected-Y eval calls inside05: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 byte0x23selected /0x03deselected — and thestylebyte values (0=line …6=dotted) are [confirmed] against the TI link-protocol var guide. - Ask-mode prompting flow (entry-line editor →
_ParseInp→_StoXfor 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’sTblRng(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]
| RAM | Role | Meaning |
|---|---|---|
0x85DE | mode / class | Caller mode at entry, then the current layout class. |
0x85DF | row index | Current row inside the selected handler or template. |
0x85E0 | slot index | Current argument or cell slot. |
0x85E1 | row count | Number of rows in the current handler record. |
0x85E2 | slot count | Number of cells or arguments in the active row. |
0x85E3..0x85E6 | saved display state | Snapshot of shared display flags while the engine redraws. |
0x85E7 | OP scratch | Saved OP1 slot used while recursing into operands. |
0x85E8 | template kind | Low nibble selects descriptor-backed template UI. |
0x85E9/0x85EA | descriptor origin | Packed pixel base used by descriptor cell mapping. |
0x85EB | row height | Pixel height for the current descriptor row. |
0x85EC/0x85ED | cell pointer | Pointer to descriptor cell data. |
0x85EE/0x85EF | fraction geometry | Measured numerator/denominator cell counts for fraction templates. |
0x85F2 | OP scratch | Second saved OP1 slot. |
0x86D7/0x86D8 | pen coordinate | Pixel coordinate staged before graph/small-font output. |
0x844B/0x844C | text row/column | Shared OS cursor row and column; 844C also participates in overflow. |
0x984A | baseline row | The row restored around recursive operand emission. |
0x9D27 | saved geometry | Copy of the measured fraction geometry used by the template handoff. |
The main draw/measure distinction comes from (IY+0x36) bit 6. Clear means the engine is
measuring or preparing state; set means it may emit pixels. Several other IY flags bias
class selection: (IY+0x09) bit 0 selects fraction/argument context, while (IY+0x02)
bits 4, 5, and 6 select exponent and alternate edit forms. [confirmed]
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:
| Class | Record | Meaning |
|---|---|---|
0x08 | 39:608B | Numeric-calculus operator row, including nDeriv( and fnInt(. |
0x0D | 39:60F9 | Fixed structural glyph rows, including direct Lintegral cells. |
0x29 | 39:6546 | Group/root-family control row. |
0x2A | 39:654D | Root/power row containing the Lroot payload cell. |
0x30 | 39:6030 | Fraction-context variant of the class-0x08 operator row. |
0x31 | 39:6433 | Stacked root/power row with a degree row. |
The display cell 00 C8 is the visible fnInt( name. It appears in class 0x08 and
class 0x30; it is distinct from the fixed Lintegral glyph cells in class 0x0D.
[confirmed]
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:
- Save display and OP state.
- Classify the current token into
0x85DE. - Load the handler record from
39:5E45. - Measure row and slot counts into
0x85E1/0x85E2. - Recurse into argument slots when a handler cell represents an operand.
- Restore the baseline row and emit visible cells during the draw pass.
The 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:
| Descriptor | Kind | Use |
|---|---|---|
39:686F | 0x10 | Fraction menu descriptor. |
39:6880 | 0x11 | Root/function template menu descriptor. |
39:6893 | descriptor family | Two-row template descriptor. |
39:689C | descriptor family | Two-row, six-column descriptor. |
39:68A5 | descriptor family | Two-row, three-column descriptor. |
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:
- A fixed
Lrootglyph supplies the hook and top shape. - The radicand is laid out as a recursive operand window.
39:5167advances the operand row window when the radicand or degree spans rows.- 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.
| Concept | Cell / glyph | Source |
|---|---|---|
fnInt( display name | 00 C8 | Class 0x08/0x30 operator records and page-1 token-name strings. |
| Fixed integral glyph | Lintegral 0x08 | Class 0x0D cells FC3F and 08 42, emitted through 39:4F1A. |
| Summation glyph | 0xC6 family | Fixed glyph data; no direct 00 C6 page-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:
- Place the tall integral glyph on the main axis.
- Use
39:5167to walk the lower, upper, integrand, and variable slots in parser order. - Update
0x844Bby the row step from39:5949. - Emit slot markers through
39:4E0A. - Emit the operand bodies through
39:5B10and39:5B1D.
The parser slot order and 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:
| Path | Entry | Use |
|---|---|---|
| Generic cell emitter | 39:4E8E | Dispatches two-byte display cells. |
| Direct large glyph map | 39:4F1A | Maps FC3C..FC40, FE7D..FE81, and xx42 cells to large-font codes. |
| String path | 39:6B66 + page 01:6D10 | Converts ordinary token cells to counted strings. |
| Display-byte remap | page 07:44DE | Remaps FE, FC, and FB prefixed display bytes. |
| Small-font blit | page 01:6293 | _VPutMap; emits small labels and compact limits from 0x86D7. |
| Large-font blit | page 07:4588 | Copies one fixed large-font glyph record. |
| Rule / rectangle helpers | 39:6ABF, 39:6AF5, 00:3555 | Draw 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]
| Address | Meaning |
|---|---|
39:4A74 | Main token/action dispatcher. |
39:4C27 | Class table lookup through 39:5E45. |
39:4DCA | Row-cell pointer computation for handler records. |
39:4DE6 | Row cell stream emitter. |
39:4E8E | Generic two-byte cell emitter. |
39:4F1A | Direct large-glyph classifier. |
39:4E0A | Argument-index marker emitter used by the row compositor. |
39:5167 | Multi-argument operand walker and tall-template row compositor. |
39:5949 | Row-step classifier for one-row versus two-row argument advance. |
39:5B10 / 39:5B1D | Saved-operand emitters used by forward and reverse placement. |
39:59E0 / 39:59F9 | Normal and variable operand emitters. |
39:672E | Template handoff for incoming 0x3D. |
39:683D | Descriptor cell-to-pixel mapper. |
39:68AE | Geometry action handler. |
39:69C8 | Descriptor/fraction geometry selector. |
39:6ABF / 39:6B1C | Fraction focus rectangle and endpoint helper. |
39:6B66 | Generic string selector. |
07:44DE | Display-byte remapper. |
07:4588 | Large-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 exercised | Path |
|---|---|---|
X^2 (raised exponent) | eqdisp_emit_subexpr2 4CA4, eqdisp_menu_or_emit 53AD | light 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 4A74 | descriptor / 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 4E0A | handler 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):
| Field | 1/2 | fnInt( | Confirms |
|---|---|---|---|
0x85E8 template kind | 0x10 | 0x00 | 0x10 = the fraction descriptor kind; 0x00 = handler-record (no descriptor). |
0x85EE/0x85EF fraction width | 2/2 | 0/0 | Measured numerator/denominator widths (each 1 cell ≈ 2 px wide). |
0x85E1 row count | — | 4 | Four 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·col → penRow)
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, returnskbdScanCodeand 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 aTIKeyCode(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 = 0x01–0x04; 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 (0435–043C) that counts the cleared (pressed, active-low) column bits into E (RLA → JR 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):
| Bit | Name | Meaning |
|---|---|---|
| 3 | shift2nd | [2nd] is pending — the next key is 2nd-shifted |
| 4 | shiftAlpha | alpha mode active — the next key is a letter |
| 5 | shiftLwrAlph | 1 = lowercase, 0 = uppercase |
| 6 | shiftALock | alpha lock — alpha survives across keys |
| 7 | shiftKeepAlph | the 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 idle →SET shift2nd(06:4AD5); loop without returning a key, lighting the 2nd cursor (IY+0x1Fbit 6).[2nd]again while pending → cancel (06:4B8ECP 0x36, clear the cursor, loop).- any other key while 2nd-pending → the 2nd handler clears the flag first (
06:4B87RES 3) then returns the 2nd-shifted code, so[2nd]is one-shot. [ALPHA](0x30) from idle →SET shiftAlpha,RES shiftLwrAlph(uppercase) (06:4AE8/4AEC); loop, lighting the alpha cursor (IY+0x1Fbit 7).[2nd]then[ALPHA](alpha pressed while 2nd is pending) → the 2nd handler reaches06:4B92CP 0x30and setsSET shiftALock+SET shiftAlpha(06:4B96/4B9A): alpha LOCK.[ALPHA]while already in alpha (06:4BFD) → in a lowercase-capable context (IY+0x24bit 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/0xFFare not tokens — they’re the menu / context-switch return codes the main event loop branches on (see 11), so_KeyToStringroutes them out viacross_page_jumprather than translating.
So the input path is: keypad → ISR → kbdScanCode → _GetKey (cooked kXxx + modifiers) → _KeyToString → token → parser (07).
Link port
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 0x08–0x0D. 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 0x09bit 5 (ready), then write the byte toport 0x0D; helper routines on page 3C manage the assist FIFO/timing. - Legacy bit-bang: to send a bit, pull one line low (write
1toport_linkfor a 0-bit,2for a 1-bit), wait for the receiver to mirror it, release, wait for idle — with a timeout that raisesE_LnkErr(0x9F, “ERR:LINK”) via_JErrorNoon 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
1. RAM state block (the link “registers”) [confirmed]
All labels below are confirmed from ti83plus.inc. This contiguous block at 0x8670 is the
silent-link control/scratch area:
| Addr | Label (.inc) | Meaning |
|---|---|---|
8670 | ioFlag | I/O state flags (bit4 tested on receive completion) |
8672 | sndRecState | transfer-type / phase: 0x0A=backup, 0x15=var DATA, 0x0B=request/dir |
8673 | ioErrState | link error sub-state |
8674 | header | packet header byte 0 = machine-ID |
8675 | header+1 | packet header byte 1 = command-ID |
8676 | header+2 | packet length, word (LE) — also the running payload byte budget |
8678 | (running) | running 16-bit checksum accumulator (sum of payload byte values) |
867D | ioData | scratch: built var-header length / data ptr setup |
867F | — | the variable header (type+name) copied from OP1 via _MovFrOP1 |
8688/8689 | ioNewData | “new var arrived” status (bit7 of 8689) |
868B | bakHeader | saved 9-byte header for echo/ACK comparison (_Mov9B to/from 8674) |
84DB | iMathPtr5 | active data pointer during a streaming transfer |
9834 | pagedCount | bytes buffered in the 16-byte staging block (Flash-write batching) |
9836 | pagedGetPtr | write cursor into pagedBuf |
983A | pagedBuf | 16-byte staging block (received data flushed to RAM/Flash 16 at a time) |
9C86 | — | HW-assist TX timeout reload (0xFA) |
9CAC | — | HW-assist TX/RX timeout down-counter (seeded from CPU speed, port 0x20) |
85D9 | varClass | variable class (backup sub-type check, =0x0A) |
IY-relative flag bytes used by the link code (IY = flags base, 0x89F0): IY+0x1B is the
link-mode/peer-type byte (which machine-ID to advertise, USB-vs-DBUS, single-byte mode), IY+0x12
bit2 “command in progress”, IY+0x24 bit1/2 transfer-active, IY+0xC bit2 APD-disable
save, IY+0x3E bit0 / IY+0x3D bit5 USB-presence.
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).
3. Packet framing — the TI link protocol [confirmed]
A TI link packet is a 4-byte header optionally followed by data + 2-byte checksum:
+--------+--------+--------+--------+ +============+----------+
| mach-ID| cmd | len-lo| len-hi| | data[len] | chk16 LE |
+--------+--------+--------+--------+ +============+----------+
8674 8675 8676 8677 streamed 8678 acc
As a C struct:
typedef struct {
uint8_t 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:
| cmd | name | seen at | meaning |
|---|---|---|---|
0x06 | VAR | _LinkXferOP reply check 4E86 CP 6 | variable header packet (type+name+size) |
0x09 | CTS | 4199 (H=0x09) | clear-to-send (receiver ready for DATA) |
0x15 | DATA | 40DA/407C send, 426D CP 0x15 recv | the variable’s data bytes |
0x2D | DEL | header-validate 4382 CP 0x2D | delete / directory variants |
0x36 | SKIP/EXIT | _LinkXferOP 4E7C CP 0x36 | peer refused this var → abort transfer |
0x56 | ACK | built by 42FB (LD H,0x56); checked 418F CP 0x56 | acknowledge |
0x5A | ERR/NAK | built by 6356/6385 (LD H,0x5A) | checksum/length error reply |
0x68 | RTS | 41BC (LD H,0x68) | request-to-send |
0x92 | EOT | 4195 (H=0x92) | end of transmission |
0xA2/0xB7 | request | _LinkXferOP 4E2B/4E2F | request 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.
5. Silent-link variable send — _LinkXferOP (3C:4DD2) [confirmed]
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 (always3C:4F3E) so that if the transfer errors out via_JError, the link state, APD timer andIY+0xCAPD 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/62BBclear the link error sub-state byte (8A0B, the low bits ofIY+0x1B-area flags).
8. Error handling [confirmed]
| Trigger | Address | Error |
|---|---|---|
| send/receive line timeout, bad echo, unexpected reply cmd | _JErrorNo 00:2799 | E_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 mismatch | 6356→ sends 0x5A NAK → 2799 | E_LnkErr 0x9F |
| peer sent SKIP/EXIT (0x36) | _LinkXferOP 4E80/4E83 | E_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]
- Host (TI-Connect, machine-ID 0x95) opens the USB/DBUS link; calc detects it (
IY+0x1Bbit1). - Host requests the directory or a specific var; calc’s receiver (
4338) parses the request header,6994/6298classify it. - To send a var:
_LinkXferOP/_SendVarCmdbuilds the VAR header (type byte + name from OP1, size) at867F, sends it (41C3, cmd path), waits forCTS(0x09). 40DAstreams theDATA(0x15) payload via_PagedGet→_SendAByte(Flash-transparent), appends the 16-bit checksum, waits forACK(0x56)._GetSysInfo(07:7345, id0x50DD)-style metadata and anEOT(0x92) close the session.- Receive direction is the mirror: header in → CTS out → DATA in (buffered 16 bytes →
RAM/Flash via
6AB1) → checksum verify (6356, NAK 0x5A on error) → ACK out → VAT store (RST5).
10. Confident address index
| space:addr | name | what |
|---|---|---|
3C:420D | _SendAByte | send one byte: HW-assist (port 0x09/0x0D) or bit-bang (port 0) |
3C:6BB2 | lnk_send_byte_hw | HW-assist send: poll port 0x09 bit5, OUT (0x0D) |
3C:443F | _RecAByteIO | receive one byte (blocking) |
3C:444A | lnk_rec_status | RX status decode (port 0x09 bit6 = TX error / bit4 = byte received / 0x19 err; sentinel 0xE0) |
3C:439C | _Rec1stByte | wait for first byte of a packet (APD + start-bit) |
3C:43A3 | _Rec1stByteNC | as above, no line-clear |
3C:41C3 | lnk_send_header | send 4-byte header (ID, cmd, len-lo, len-hi) |
3C:419B | lnk_send_ctrl_pkt | send a 0-length control packet (cmd in H) |
3C:4195 | lnk_send_eot | send EOT (cmd 0x92) |
3C:4199 | lnk_send_cts | send CTS (cmd 0x09) |
3C:4338 | lnk_recv_header | receive + validate 4-byte header |
3C:620A | lnk_local_machine_id | pick local machine-ID from IY+0x1B mode |
3C:42FB | lnk_send_ack | build+send ACK (cmd 0x56, fresh local machine-ID), restoring the saved header |
3C:4292 | lnk_recv_data | receive DATA payload, 16-byte Flash batching, checksum |
3C:6356 | lnk_verify_cksum | verify count vs len; NAK 0x5A on mismatch |
3C:6AB1 | lnk_flush_block | flush 16-byte staging block to RAM/Flash (port 0x14) |
3C:4DD2 | _LinkXferOP | silent-link variable send orchestrator (OP1=name) |
3C:4EDD | _SendVarCmd | bcall _SendVarCmd (4A14) body; DI-wrapped send-by-name |
3C:4763 | lnk_resolve_var | resolve var class/size/ptr for sending (archive-aware) |
3C:40DA | lnk_send_data | send DATA payload (_PagedGet→_SendAByte) + checksum + ACK wait |
3C:4167 | lnk_send_cksum_tail | append 16-bit checksum, recv reply, expect ACK 0x56 |
3C:4F3E | lnk_cleanup | error/abort cleanup (restore APD/timers/flags) |
3C:62B0 | lnk_clear_substate | clear link error sub-state (8A0B) |
3C:6994 | lnk_recv_store | receive var + VAT store sequence (expects 0x09 then 0x15) |
00:278D | _ErrLinkXmit | _JError(0x9F) E_LnkErr |
00:2799 | _JErrorNo | raise current pending error (link → 0x9F) |
07:7345 | _GetSysInfo (id 0x50DD) | system info reply (used in link sessions) |
00:4A14 | _SendVarCmd (bcall id) | → 3C:4EDD |
Ports: 0x00 = bit-bang link (tip/ring); 0x08–0x0D = HW link-assist control/status/data
FIFO (port 0x09 bit5 TX-ready, bit6 transmission-error, bit4 byte-received, bits 0x19 error);
0x02 bit7 = non-83+-Basic (assist-present gate on 84+; WikiTI’s “link-assist available” is bit6);
0x20 = CPU speed (timeout scaling); 0x4D bits5/6 = USB negotiation; 0x14 = Flash
write/erase (received-to-archive path). See sub-usb-asic.md for the assist port
state machine. RAM block: ioFlag 8670 … bakHeader 868B, staging
pagedBuf 983A.
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/Receivebcalls) 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:
_LinkXferOPcallsram:2E0B, across_page_jumpthunk to35:4280, after sampling port0x4D.
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:
| Layer | Port range | Role |
|---|---|---|
| Legacy link | 0x00 | 2.5 mm tip/ring open-collector byte path. [confirmed: 3C:6C99, 3C:6CF3] |
| Link-assist FIFO | 0x08–0x0D | Hardware byte send/receive assist used below _SendAByte and _RecAByteIO. [confirmed: 3C:6BB1–6D53] |
| USB line / interrupt gates | 0x4D, 0x55, 0x56 | Line-state and event/status gates used before and during link handling. [confirmed: 3C:4E4A, 00:006F] |
| USB controller / endpoints | 0x4A–0x5B, 0x80–0xA2 | Page-35 USB host/device stack, including setup, endpoint FIFOs, callbacks, and data transfer. [confirmed: 35:4031–5B9B] |
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]
| Port | Observed use in OS 2.55MP | Evidence |
|---|---|---|
0x02 | Hardware/model gate before using assist paths. The link code tests bit 7 before touching ports 0x08–0x0D. | 3C:6C82, 3C:6CB8, 3C:6D15 |
0x08 | Link-assist control/idle latch. The OS writes 0x80 when clearing an inactive/error-free assist state, and 0x00 when marking the assist state active. | OUT (0x08) at 3C:6C4D/6C50, 3C:6D48, 3C:6D5B |
0x09 | Link-assist status on reads. Bit 5 is TX-ready; bit 6 is a transmission/error condition; bit 4 marks a received byte. Masks 0x19, 0x58, and 0x99 are used as error/activity predicates. On writes, the OS setup value 0x97 matches WikiTI’s CPU-speed-0 signaling-rate register. | 3C:6BB6–6BC5, 3C:444A, 3C:6BFA, 3C:6CCE, 3C:6D33; WikiTI port 09 |
0x0A | Assist receive/data register on reads; the confirmed receive path reads the byte here. On writes, the OS setup value 0xB4 matches WikiTI’s CPU-speed-1 signaling-rate register. TilEm models reads as “last received byte” and stores writes as opaque assist state. | 3C:6C20, 3C:6C2B, 3C:6C39; WikiTI port 0A; TilEm x4_io.c |
0x0B, 0x0C | Assist signaling-rate configuration for CPU speed modes 2 and 3, initialized with 0xB4. The ROM byte-transfer path writes them during setup but does not read them back. TilEm stores the writes without emulating timing from the values. | 3C:6C3D, 3C:6C3F; WikiTI ports 0B/0C; TilEm x4_io.c |
0x0D | Assist TX FIFO/data register. _SendAByte writes the outgoing byte here after port 0x09 bit 5 becomes set. | 3C:6BBC–6BBF |
0x20 | CPU speed bit used to select assist/link wait-loop reloads. The send timeout uses 0xFFFF when bit 0 is set and 0x6800 when clear. | 3C:6BCC, 3C:6C8B, 3C:6CC1 |
0x4C | USB controller handshake/status byte. The page-35 stack compares it with 0x5A/0x1A and 0x12/0x52, and clears or primes it with 0x00/0x08 during setup. TilEm returns 0x22 to make the calc see no attached USB peer. | 35:42B7, 35:42F6, 35:403C, 35:40E6; TilEm x4_io.c |
0x4D | USB line-state gate. _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:4E4A–4E6F, 35:42BF, 35:4B6A–4B9F; TilEm x4_io.c |
0x55 | USB interrupt status, active-low in the low five bits. The IM1 dispatcher tests (in(0x55) ^ 0xFF) & 0x1F first. | 00:006F–0075 |
0x56 | USB line-event bitmap used by the IM1 dispatcher after port 0x55 reports USB activity. Bits 4, 5, 6, 7, and 1 dispatch to page-35 handlers through page-0 bjumps. | 00:0085–00AE, 00:0113–0127 |
0x57, 0x5B, 0x4A, 0x54 | USB controller control/ack registers used by page-35 setup and event handlers. The ROM confirms values such as 0x10, 0x20, 0x22, 0x50, 0x80, 0x90, 0x93 on 0x57, 0x00/0x01 on 0x5B, 0x20 on 0x4A, and 0x02/0x44/0xC4 on 0x54. | 35:4038–4060, 35:42C5–42EA, 35:4B6A–4C14 |
0x80–0xA2 | Endpoint/status/FIFO region used by the public USB API. Examples: _SendUSBData writes 64-byte chunks to 0xA2; _RequestUSBData reads 8-byte records from 0xA1; setup/config paths write descriptor bytes through 0xA0 and use selector/status ports 0x8E, 0x8F, 0x91, 0x94, and 0x98. | 35:4DD3, 35:470B, 35:48BA, 35:48F8 |
The 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:
- Seed the inner retry counter at RAM
0x9C86with0xFA. - Read port
0x09. - If bit 5 is set, copy the outgoing byte from
Cto port0x0Dand return. - If bit 5 is clear, call the timeout decrementer (
3C:6BE4/lnk_timeout_dec) and retry until the outer counter at0x9CACexpires, then fall into the link error path at3C:4434.
The ROM disassembles to:
; 3C:6BB2, assist send path
6BB2: CALL 6D4Fh ; clear/prepare assist I/O latch
6BB5: CALL 6BD2h ; seed 9CAC from CPU speed
6BB8: CALL 6BD2h
6BBB: LD A,0FAh
6BBD: LD (9C86h),A ; inner retry reload
6BC0: IN A,(09h)
6BC2: BIT 5,A
6BC4: JR Z,6BCAh ; TX not ready
6BC6: LD A,C
6BC7: OUT (0Dh),A ; write byte to assist FIFO
6BC9: RET
6BCA: CALL 6BE4h ; decrement 9CAC, Z means keep polling
6BCD: JR Z,6BBBh
6BCF: JP 4434h ; link timeout/error path
lnk_set_timeout (3C:6BD2) seeds 0x9CAC from CPU speed. When port 0x20 bit 0 is clear it uses
0x6800; when the bit is set it leaves the larger 0xFFFF seed. The ROM confirms the two reload
values, while the wall-clock timeout they target is not measured here. [confirmed]
Receiving and status handling [confirmed]
The receive path is split between _RecAByteIO (3C:443F), lnk_rec_status (3C:444A), and the
assist helpers around 3C:6BF4–6D40.
The hardware-facing receive loop waits until port 0x09 & 0x58 becomes nonzero. In the confirmed
path:
0x40(bit 6) is treated as a transmission/error condition.0x10(bit 4) is the “byte received” condition.0x08is an assist read-busy/activity bit: it wakes the wait loop, but the byte is not accepted until bit 4 or an error/status bit is also present. TilEm names the corresponding stateTILEM_LINK_ASSIST_READ_BUSY.- When the receive condition is accepted, the byte is read from port
0x0AintoC. - The status masks
0x19and0x99select error/activity cases before the code resets or re-arms the assist latch through port0x08.
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:
| Port | CPU speed mode | Value | Divisor field | Wait field |
|---|---|---|---|---|
0x09 | 0, 6 MHz | 0x97 (10010111b) | 100b → divide by 16 | 0x17 |
0x0A | 1 | 0xB4 (10110100b) | 101b → divide by 32 | 0x14 |
0x0B | 2, 15 MHz duplicate 1 | 0xB4 (10110100b) | 101b → divide by 32 | 0x14 |
0x0C | 3, 15 MHz duplicate 2 | 0xB4 (10110100b) | 101b → divide by 32 | 0x14 |
Direct ROM scans found the page-3C byte-transfer path writing those constants during setup, then
using the read side of 0x09 for status and 0x0A for received bytes. TilEm agrees on the runtime
status/data behavior and stores ports 0x09–0x0C, but its x4/xn/xs/xz models label the
write-side settings as unknown or timeout-like and do not derive link timing from 0x97/0xB4.
[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:
OP1holds the variable type/name.sndRecState(0x8672) is0x15for DATA-style receive.IY+0x1Bbit 0 selects USB-first behavior; reset means use the link port path.
The OS confirms that contract in the 4E35–4E73 gate:
- If
IY+0x1Bbit 0 is clear, it skips USB probing and sends through the ordinary link path. - If bit 0 is set and either
IY+0x1Bbit 5 or bit 6 asks for USB handling, it reads port0x4D. - If port
0x4Dbit 5 is clear, or bit 5 is set and bit 6 is clear, the OS setsIY+0x1Bbit 5 and calls the page-0 bjump atram:2E0B. ram:2E0Bdispatches via inline descriptor80 42 75, which is target35:4280after the normal page mask. That routine calls the public_InitUSBDevicebody at35:42B0, then accepts only TI vendor0x0451with product IDs0xE003,0xE008, or0xE00F; success returns carry clear, while mismatch or init failure returns carry set.- On carry set,
_LinkXferOPclearsIY+0x1Bbit 5 and continues intolnk_send_data_867d(3C:4055), which sends the same TI link request/VAR/DATA packets described in the link-transfer page. - On carry clear, the USB path remains selected and the OS calls the bjump reached through
ram:3FC3withA=0x0A.
This makes _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 bit | Page-0 dispatch | Page-35 target | Observed role |
|---|---|---|---|
| 4 | 00:0122 → ram:3FA5 | 35:4B6A | line/event settle path; waits on 0x4D bits 7 and 0, writes 0x57 = 0x22. |
| 5 | 00:0127 → ram:3FAB | 35:4B9F | event clear/re-arm path; may clear 0x4C, reset USBFlag2 bit 6, and write 0x57 = 0x50/0x93. |
| 6 | 00:0113 → ram:3F93 | 35:40B2 | USB setup path; sets IY+0x1B bit 5, initializes controller state, and waits for 0x4C = 0x1A/0x5A. |
| 7 | 00:0118 → ram:3F99 | 35:4C14 | cleanup/reset path; clears 0x5B, resets USBFlag2 bit 0, and jumps through the common controller reset. |
| 1 | 00:011D → ram:3F9F | 35:4031 | alternate setup path; waits for 0x4C = 0x12/0x52 and uses endpoint/status ports 0x87/0x89/0x8B. |
[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:01B1–01DB]
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 ID | Public name | Body | ROM-grounded behavior |
|---|---|---|---|
50F2 | _SendUSBData | 35:4DD3 | Sends from HL with byte count in DE; stores progress at 0x9C7E/0x9C81 and writes 64-byte chunks to port 0xA2. |
50F5 | _AppGetCBLUSB | 3B:54C7 | Sets IY+0x1B bit 1, clears bit 2, then reaches _GetVarCmdUSB. |
50F8 | _AppGetCalcUSB | 3B:54F0 | At 3B:54DE clears IY+0x16 bit 0 and sets sndRecState=0x15, then bcall 0x50FB (shared get-var path). |
50FB | _GetVarCmdUSB / _LinkXferOP | 3C:4DD2 | USB-first variable command wrapper described above. |
5254 | _InitUSBDeviceCallback | 35:4696 | Initializes device mode, stores callback page/address at 0x9C13/0x9C14, and returns 0xFC–0xFF style error bytes with carry set on failure. |
5257 / 5311 | _KillUSBDevice / _RecycleUSB | 35:46FC / 35:5B9B | Clears callback state and recycles through the same cleanup path. |
525A | _SetUSBConfiguration | 35:470B | Builds an 8-byte request block at 0x9C29 and writes it through port 0xA0. |
525D / 5260 | _RequestUSBData / _StopReceivingUSBData | 35:48BA / 35:48D1 | Stores or clears the receive-buffer descriptor at 0x9C1E; receive records are read from port 0xA1. |
528A / 528D | _EnableUSBHook / _DisableUSBHook | 3B:7DC6 / 3B:7DD1 | Stores USBActivityHookPtr/page at 0x9BD4/0x9BD6 and toggles (IY+0x3A) bit 0. |
5290 | _InitUSBDevice | 35:42B0 | Main controller/device initialization path; uses 0x4C/0x4D line handshakes and endpoint ports 0x80–0x9B. |
5293 | _KillUSBPeripheral | 35:59CF | Peripheral teardown; sets controller state 0x9C28 = 5 and manipulates ports 0x54/0x81. |
530B | _ToggleUSBSmartPadInput | 35:5B84 | Sets or clears bit 3 in 0x9C75 according to A == 1. |
530E | _IsUSBDeviceConnected | 35:5B92 | Preserves A; returns flags from IN (0x81) & 0x40 (bit 6). (The .inc comment guesses bit 4,(81h), but the body actually masks bit 6.) |
How to use it in code [grounded by OS calls]
Prefer the OS entry points unless the program is deliberately writing a USB driver:
| Need | OS surface | ROM support |
|---|---|---|
| Send or request a variable over USB/link | _GetVarCmdUSB/_LinkXferOP (50FB → 3C:4DD2) or _SendVarCmd (4A14 → 3C:4EDD) | Packet engine and USB-selection gate confirmed on page 3C. 0x50FB is _GetVarCmdUSB in ti83plus.inc. |
| Send one byte on the active link transport | _SendAByte (4EE5 → 3C:420D) | Assist branch writes C to port 0x0D after port 0x09 bit 5. |
| Receive one byte on the active link transport | _RecAByteIO (4F03 → 3C:443F) | Status path checks port 0x09 and reads port 0x0A on the assist path. |
| Use the raw assist FIFO | Poll port 0x09 bit 5, then write the byte to port 0x0D; for receive, observe port 0x09 bit 4/error bits and read port 0x0A. | Confirmed as an OS pattern, but not a complete public API. |
The raw FIFO sequence is only the byte layer. A working transfer still needs the packet layer: machine ID, command, length, payload checksum, ACK/NAK, and EOT. That framing is documented in sub-link-transfer.md.
Practical rules:
- Set up
IY+0x1Bconsistently before calling_LinkXferOP. Bit 0 is the USB-first selector. - Do not write ports
0x08–0x0Dwhile the OS link engine is active; the OS keeps state inIY+0x3Ebit 0,0x9C86, and0x9CAC. - If a custom interrupt handler is installed, either chain to the OS handler or service the same
source gates. The OS itself expects to handle port
0x55/0x56events. - Use the public USB bcalls for endpoint/controller work. The raw page-35 endpoint ports are mapped well enough to identify the FIFOs and state variables, but their bit-level protocol is not a stable public API.
Limits
- The ROM calls
ram:2E0B, across_page_jumpthunk to35:4280. Its carry-clear/carry-set result is decoded above. - The public
0x50xx/0x52xx/0x53xxUSB APIs are mapped to bodies and sampled above. The boot-page0x8xxxUSB names (_InitUSB,_KillUSB,_AttemptUSBOSReceive,_ReceiveOS_USB,_USBErrorCleanup) remain part of the repository-wide0x8xxxbcall-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/0xB4field 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.
| bcall | ID | Body (page:addr) |
|---|---|---|
_AbsO1O2Cp | 410E | 00:1987 |
_AbsO1PAbsO2 | 405A | 00:225B |
_ACos | 40DE | 02:76DF |
_ACosH | 40F0 | 02:7964 |
_ACosRad | 40D2 | 02:76C9 |
_AdrLEle | 462D | 02:47C5 |
_AdrMEle | 4609 | 02:4002 |
_AdrMRow | 4606 | 02:4000 |
_AllEq | 4876 | 04:6218 |
_AllocFPS | 43A5 | 00:1534 |
_AllocFPS1 | 43A8 | 00:1537 |
_Angle | 4102 | 02:6A38 |
_AnsName | 4B52 | 38:74B7 |
_ApdSetup | 4C93 | 00:03AE |
_app_5de7 | 5326 | 3D:5DE7 |
_AppGetCalc | 4C78 | 3B:54EC |
_AppGetCbl | 4C75 | 3B:54C3 |
_AppInit | 404B | 00:0936 |
_arc_5936 | 51AC | 07:5936 |
_arc_59f1 | 4A68 | 07:59F1 |
_Arc_Unarc | 4FD8 | 07:6248 |
_ASin | 40E4 | 02:76F1 |
_ASinH | 40ED | 02:7956 |
_ASinRad | 40DB | 02:76DA |
_ATan | 40E1 | 02:76E9 |
_ATan2 | 40E7 | 02:7749 |
_ATan2Rad | 40D8 | 02:76D4 |
_ATanH | 40EA | 02:7909 |
_ATanRad | 40D5 | 02:76CF |
_BinOPExec | 4663 | 02:53DD |
_Bit_VertSplit | 4FA8 | 00:215D |
_BufClr | 5074 | 04:6074 |
_BufCpy | 5071 | 04:60A6 |
_CAbs | 4E97 | 02:6C47 |
_CAdd | 4E88 | 02:6BA5 |
_CanAlphIns | 4C69 | 00:04C6 |
_CDiv | 4E94 | 02:6BF3 |
_CDivByReal | 4EBB | 02:6DAC |
_CEtoX | 4EA9 | 02:6D1D |
_CFrac | 4EC1 | 02:6DCF |
_CheckSplitFlag | 49F0 | 00:2060 |
_Chk_Batt_Low | 50B3 | 00:0D07 |
_ChkFindSym | 42F1 | 00:0E60 |
_CIntgr | 4EC4 | 02:6DDD |
_CircCmd | 47D4 | 33:74CE |
_CkInt | 4234 | 00:1E06 |
_CkOdd | 4237 | 00:1E0A |
_CkOP1C0 | 4225 | 00:1DE4 |
_CkOP1Cplx | 40FC | 00:193A |
_CkOP1FP0 | 4228 | 00:1DE9 |
_CkOP1Pos | 4258 | 00:1E5D |
_CkOP1Real | 40FF | 00:1942 |
_CkOP2FP0 | 422B | 00:1DEE |
_CkOP2Pos | 4255 | 00:1E58 |
_CkOP2Real | 42DF | 00:214E |
_CkPosInt | 4231 | 00:1DFD |
_CkValidNum | 4270 | 00:1E9B |
_CleanAll | 4A50 | 07:52CF |
_ClearRect | 4D5C | 3B:6935 |
_ClearRow | 4CED | 01:6934 |
_CLine | 4798 | 33:6028 |
_CLineS | 479B | 33:6034 |
_CLN | 4EA0 | 02:6CCA |
_CLog | 4EA3 | 02:6CE7 |
_CLogPrep | 50FE | 02:6F1B |
_CloseEditBuf | 48D3 | 05:5675 |
_CloseEditBufNoR | 476E | 03:4743 |
_CloseEditEqu | 496C | 06:4771 |
_CloseProg | 4A35 | 07:4FB4 |
_ClrGraphRef | 4A38 | 07:4FD8 |
_ClrLCD | 4543 | 01:60F5 |
_ClrLCDFull | 4540 | 01:60E4 |
_ClrLp | 41D1 | 00:1BC4 |
_ClrOP1S | 425E | 00:1E68 |
_ClrOP2S | 425B | 00:1E63 |
_ClrScrn | 4549 | 01:6167 |
_ClrScrnFull | 4546 | 01:6162 |
_ClrTxtShd | 454C | 01:616F |
_CMltByReal | 4EB8 | 02:6D94 |
_CmpSyms | 4A4A | 07:519E |
_CMult | 4E8E | 02:6BB7 |
_Conj | 4EB5 | 02:6D8F |
_ConvDim | 4B43 | 38:741F |
_ConvDim00 | 4B46 | 38:7422 |
_ConvLcToLr | 4A23 | 07:4CFF |
_ConvLrToLc | 4A56 | 07:5368 |
_ConvOP1 | 4AEF | 38:7433 |
_COP1Set0 | 4105 | 00:195F |
_Cos | 40C0 | 02:7346 |
_CosH | 40CC | 02:762E |
_CpHLDE | 400C | 00:21BB |
_CplxOPArrange | 4648 | 02:494F |
_CPoint | 4DC8 | 04:43D8 |
_CPointS | 47F5 | 04:43DD |
_CpOP1OP2 | 4111 | 00:198D |
_CpOP4OP3 | 4108 | 00:197A |
_CpyO1ToFPS1 | 445C | 00:16D4 |
_CpyO1ToFPS2 | 446B | 00:16ED |
_CpyO1ToFPS3 | 4477 | 00:1701 |
_CpyO1ToFPS4 | 4489 | 00:172B |
_CpyO1ToFPS5 | 4483 | 00:171C |
_CpyO1ToFPS6 | 447D | 00:170B |
_CpyO1ToFPS7 | 4480 | 00:1712 |
_CpyO1ToFPST | 444A | 00:16B5 |
_CpyO2ToFPS1 | 4459 | 00:16CF |
_CpyO2ToFPS2 | 4462 | 00:16DE |
_CpyO2ToFPS3 | 4474 | 00:16FC |
_CpyO2ToFPS4 | 4486 | 00:1726 |
_CpyO2ToFPST | 4444 | 00:16AB |
_CpyO3ToFPS1 | 4453 | 00:16C5 |
_CpyO3ToFPS2 | 4465 | 00:16E3 |
_CpyO3ToFPST | 4441 | 00:16A6 |
_CpyO5ToFPS1 | 4456 | 00:16CA |
_CpyO5ToFPS3 | 4471 | 00:16F7 |
_CpyO6ToFPS2 | 4468 | 00:16E8 |
_CpyO6ToFPST | 4447 | 00:16B0 |
_CpyStack | 4429 | 00:167C |
_CpyTo1FPS1 | 4432 | 00:168D |
_CpyTo1FPS10 | 43F3 | 00:1617 |
_CpyTo1FPS11 | 43D8 | 00:15CF |
_CpyTo1FPS2 | 443B | 00:169C |
_CpyTo1FPS3 | 4408 | 00:1647 |
_CpyTo1FPS4 | 440E | 00:1651 |
_CpyTo1FPS5 | 43DE | 00:15DF |
_CpyTo1FPS6 | 43E4 | 00:15EF |
_CpyTo1FPS7 | 43EA | 00:15FE |
_CpyTo1FPS8 | 43ED | 00:1608 |
_CpyTo1FPS9 | 43F6 | 00:1621 |
_CpyTo1FPST | 4423 | 00:1674 |
_CpyTo2FPS1 | 442F | 00:1688 |
_CpyTo2FPS2 | 4438 | 00:1697 |
_CpyTo2FPS3 | 4402 | 00:163F |
_CpyTo2FPS4 | 43F9 | 00:162B |
_CpyTo2FPS5 | 43DB | 00:15DA |
_CpyTo2FPS6 | 43E1 | 00:15EA |
_CpyTo2FPS7 | 43E7 | 00:15F9 |
_CpyTo2FPS8 | 43F0 | 00:160D |
_CpyTo2FPST | 4420 | 00:166F |
_CpyTo3FPS1 | 442C | 00:1683 |
_CpyTo3FPS2 | 4411 | 00:1656 |
_CpyTo3FPST | 441D | 00:166A |
_CpyTo4FPST | 441A | 00:1665 |
_CpyTo5FPST | 4414 | 00:165B |
_CpyTo6FPS2 | 43FF | 00:163A |
_CpyTo6FPS3 | 43FC | 00:1635 |
_CpyTo6FPST | 4417 | 00:1660 |
_CpyToFPS1 | 445F | 00:16D7 |
_CpyToFPS2 | 446E | 00:16F0 |
_CpyToFPS3 | 447A | 00:1704 |
_CpyToFPST | 444D | 00:16B8 |
_CpyToStack | 4450 | 00:16BD |
_Create0Equ | 432A | 00:1131 |
_CreateAppVar | 4E6A | 00:114B |
_CreateCList | 431B | 00:1109 |
_CreateCplx | 430C | 00:10B0 |
_CreateEqu | 4330 | 00:113C |
_CreatePair | 4B0D | 38:6785 |
_CreatePict | 4333 | 00:1140 |
_CreateProg | 4339 | 00:1153 |
_CreateProtProg | 4E6D | 00:114F |
_CreateReal | 430F | 00:10B8 |
_CreateRList | 4315 | 00:10C4 |
_CreateRMat | 4321 | 00:1115 |
_CreateStrng | 4327 | 00:1123 |
_CRecip | 4E91 | 02:6BE6 |
_CrystalTimerA | 4B49 | 34:5A99 |
_CrystalTimerB | 4B4C | 34:5A9D |
_CrystalTimerC | 4B4F | 34:5AA1 |
_CSqRoot | 4E9D | 02:6C84 |
_CSquare | 4E8B | 02:6BB4 |
_CSub | 4E85 | 02:6BA2 |
_CTenX | 4EA6 | 02:6D08 |
_CTrunc | 4EBE | 02:6DBD |
_Cube | 407B | 00:237D |
_CursorOff | 45BE | 06:7C5F |
_CursorOn | 45C4 | 06:7D34 |
_CXrootY | 4EAC | 02:6D3B |
_CYtoX | 4EB2 | 02:6D5C |
_DarkLine | 47DD | 04:4025 |
_DarkPnt | 47F2 | 04:43D6 |
_DataSize | 436C | 00:1485 |
_DataSizeA | 4369 | 00:1466 |
_DeallocFPS | 439F | 00:1526 |
_DeallocFPS1 | 43A2 | 00:152A |
_DecO1Exp | 4267 | 00:1E6F |
_DelListEl | 4A2F | 07:4F43 |
_DelMem | 4357 | 00:1368 |
_DelRes | 4A20 | 07:72F5 |
_DelVar | 4351 | 00:1308 |
_DelVarArc | 4FC6 | 00:12D9 |
_DelVarNoArc | 4FC9 | 00:130E |
_DisableApd | 4C84 | 3B:7AA8 |
_Disp | 4F45 | 37:51D3 |
_DispDone | 45B5 | 01:69B0 |
_DispEOL | 45A6 | 01:689F |
_DispHL | 4507 | 01:5BF6 |
_DisplayImage | 4D9B | 3B:6A72 |
_DispMenuTitle | 5065 | 39:4D21 |
_DispOP1A | 4BF7 | 04:7844 |
_DispPagedStr | 51CA | 01:7C4D |
_DivHLBy10 | 400F | 00:0269 |
_DivHLByA | 4012 | 00:026B |
_DrawCirc2 | 4C66 | 3B:7171 |
_DrawCmd | 48C1 | 04:7B8B |
_DrawRectBorder | 4D7D | 3B:68F5 |
_DrawRectBorderClear | 4D8C | 3B:692A |
_DrawZeroOP1 | 4873 | 04:620B |
_drw_5df1 | 53F5 | 04:5DF1 |
_drw_5df4 | 4825 | 04:5DF4 |
_drw_638e | 487C | 04:638E |
_dsp_6240 | 4C42 | 01:6240 |
_dsp_65ea | 4D6B | 01:65EA |
_DToR | 4075 | 00:236B |
_EditProg | 4A32 | 07:4F6B |
_edt_5d6f | 5464 | 03:5D6F |
_edt_69f8 | 5461 | 03:69F8 |
_edt_6bd1 | 5458 | 03:6BD1 |
_EnableApd | 4C87 | 3B:7AAD |
_EnoughMem | 42FD | 00:0FA6 |
_EOP1NotReal | 4279 | 00:1F06 |
_Equ_or_NewEqu | 42C4 | 00:20FD |
_EraseEOL | 4552 | 01:61C5 |
_EraseRectBorder | 4D86 | 3B:68F1 |
_ErrArgument | 44AD | 00:2711 |
_ErrBadGuess | 44CB | 00:2751 |
_ErrBreak | 44BF | 00:273D |
_ErrD_OP1_0 | 42D3 | 00:212D |
_ErrD_OP1_LE_0 | 42D0 | 00:212A |
_ErrD_OP1Not_R | 42CA | 00:2120 |
_ErrD_OP1NotPos | 42C7 | 00:2119 |
_ErrD_OP1NotPosInt | 42CD | 00:2125 |
_ErrDataType | 44AA | 00:2708 |
_ErrDimension | 44B3 | 00:2719 |
_ErrDimMismatch | 44B0 | 00:2715 |
_ErrDivBy0 | 4498 | 00:26EC |
_ErrDomain | 449E | 00:26F4 |
_ErrIncrement | 44A1 | 00:26F8 |
_ErrInvalid | 44BC | 00:2729 |
_ErrIterations | 44C8 | 00:274D |
_ErrLinkXmit | 44D4 | 00:278D |
_ErrMemory | 44B9 | 00:2721 |
_ErrNon_Real | 44A4 | 00:26FC |
_ErrNonReal | 4A8C | 38:42E1 |
_ErrNotEnoughMem | 448C | 00:1735 |
_ErrOverflow | 4495 | 00:26E8 |
_ErrSignChange | 44C5 | 00:2749 |
_ErrSingularMat | 449B | 00:26F0 |
_ErrStat | 44C2 | 00:2741 |
_ErrStatPlot | 44D1 | 00:2759 |
_ErrSyntax | 44A7 | 00:2700 |
_ErrTolTooSmall | 44CE | 00:2755 |
_ErrUndefined | 44B6 | 00:271D |
_EToX | 40B4 | 02:705C |
_Exch9 | 43D5 | 00:15CA |
_ExLp | 4222 | 00:1DDA |
_ExpToHex | 424F | 00:1E4E |
_Factorial | 4B85 | 35:7995 |
_FillRect | 4D62 | 3B:6939 |
_FillRectPattern | 4D89 | 3B:6814 |
_Find_Parse_Formula | 4AF2 | 38:758A |
_FindAlphaDn | 4A47 | 07:50B8 |
_FindAlphaUp | 4A44 | 07:50B5 |
_FindApp | 4C4E | 3D:5EE3 |
_FindAppDn | 4C4B | 3D:5DE6 |
_FindAppNumPages | 509B | 3D:4AA3 |
_FindAppUp | 4C48 | 3D:5DDA |
_FindSym | 42F4 | 00:0E65 |
_FiveExec | 467E | 02:69BC |
_FixTempCnt | 4A3B | 07:4FEC |
_FlashToRam | 5017 | 3D:6745 |
_ForceFullScreen | 508F | 39:66D2 |
_FormBase | 50AA | 06:57C0 |
_FormDCplx | 4996 | 06:59D3 |
_FormEReal | 4990 | 06:5799 |
_FormReal | 4999 | 06:5ACF |
_FourExec | 467B | 02:6889 |
_FPAdd | 4072 | 00:229E |
_FPDiv | 4099 | 00:2541 |
_FPMult | 4084 | 00:238B |
_FPRecip | 4096 | 00:253D |
_FPSPushReal | 4A83 | 07:6365 |
_FPSquare | 4081 | 00:238A |
_FPSub | 406F | 00:2297 |
_fpx_4a7b | 5185 | 02:4A7B |
_fpx_5d70 | 4669 | 02:5D70 |
_fpx_5dbb | 466C | 02:5DBB |
_fpx_7069 | 5101 | 02:7069 |
_fpx_7d9d | 533B | 02:7D9D |
_fpx_7dfe | 5338 | 02:7DFE |
_Frac | 4093 | 00:24E3 |
_Get_Tok_Strng | 4594 | 01:66EA |
_GetBaseVer | 4C6F | 00:0284 |
_GetCSC | 4018 | 00:04B2 |
_GetKey | 4972 | 06:491E |
_GetLToOP1 | 4636 | 02:47EA |
_GetMToOP1 | 4615 | 02:4044 |
_GetPosListElem | 4666 | 02:5BBB |
_GetSysInfo | 50DD | 07:7345 |
_GetTokLen | 4591 | 01:66E5 |
_GraphParseTok | 510A | 33:5023 |
_GraphTblFind | 47CB | 33:7097 |
_GraphTblNext | 47C8 | 33:707A |
_GrBufClr | 4BD0 | 04:6071 |
_GrBufCpy | 486A | 04:60A3 |
_grc_454b | 5263 | 37:454B |
_grc_4556 | 5266 | 37:4556 |
_grc_4575 | 5269 | 37:4575 |
_grc_4611 | 52FF | 37:4611 |
_grc_51c2 | 517F | 37:51C2 |
_grc_5223 | 51A0 | 37:5223 |
_grc_5d44 | 51D6 | 37:5D44 |
_grc_5f42 | 5200 | 37:5F42 |
_grc_60cb | 51FA | 37:60CB |
_grf_435f | 5140 | 33:435F |
_grf_5e06 | 5476 | 33:5E06 |
_grf_7066 | 47C5 | 33:7066 |
_GrphCirc | 47D7 | 33:758D |
_HLTimes9 | 40F9 | 00:1930 |
_homeup | 4558 | 01:6216 |
_HorizCmd | 48A6 | 04:793E |
_HTimesL | 4276 | 00:1EF6 |
_IBounds | 4C60 | 04:42EC |
_IBoundsFull | 4D98 | 04:4306 |
_ILine | 47E0 | 04:4029 |
_IncLstSize | 4A29 | 07:4EF4 |
_InsertList | 4A2C | 07:4F07 |
_InsertMem | 42F7 | 00:0F81 |
_Int | 40A5 | 00:2621 |
_Intgr | 405D | 00:2263 |
_InvCmd | 48C7 | 04:7D6A |
_InvertRect | 4D5F | 3B:693D |
_InvOP1S | 408D | 00:24BD |
_InvOP1SC | 408A | 00:24BA |
_InvOP2S | 4090 | 00:24CD |
_InvSub | 4063 | 00:227D |
_IOffset | 4C63 | 04:42B5 |
_IPoint | 47E3 | 04:4157 |
_IsA2ByteTok | 42A3 | 00:1FE8 |
_IsEditEmpty | 492D | 00:21A7 |
_JError | 44D7 | 00:2793 |
_JErrorNo | 4000 | 00:2799 |
_JForceCmd | 402A | 00:0747 |
_JForceCmdNoChar | 4027 | 00:0746 |
_JForceGraphKey | 5005 | 01:6BFD |
_JForceGraphNoKey | 5002 | 01:6BFB |
_KeyToString | 45CA | 01:6D10 |
_LCD_DRIVERON | 4978 | 06:4D02 |
_LcdBlitRegion | 4D26 | 07:5431 |
_LdHLind | 4009 | 00:0033 |
_LineCmd | 48AC | 04:796A |
_LinkXferOP | 50FB | 3C:4DD2 |
_ListIdxTimes9 | 53D1 | 35:79E9 |
_lnk_62b0 | 5182 | 3C:62B0 |
_LnX | 40AB | 02:6EFD |
_Load_SFont | 4783 | 03:4A8F |
_LoadCIndPaged | 501D | 00:029F |
_LoadDEIndPaged | 501A | 3C:6B36 |
_LoadPattern | 4CB1 | 01:6267 |
_LogX | 40AE | 02:6F16 |
_Max | 4057 | 00:224D |
_mde_7da9 | 49DB | 36:7DA9 |
_MemChk | 42E5 | 00:0E20 |
_MemClear | 4C30 | 3B:7138 |
_MemSet | 4C33 | 3B:7139 |
_Min | 4054 | 00:2244 |
_Minus1 | 406C | 00:2294 |
_mnu_6ddb | 5467 | 39:6DDB |
_Mov10B | 415C | 00:1A90 |
_Mov18B | 47DA | 00:192B |
_Mov7B | 4168 | 00:1A96 |
_Mov8B | 4165 | 00:1A94 |
_Mov9B | 415F | 00:1A92 |
_Mov9OP1OP2 | 417D | 00:1B06 |
_Mov9OP2Cp | 410B | 00:1982 |
_Mov9ToOP1 | 417A | 00:1B01 |
_Mov9ToOP2 | 4180 | 00:1B07 |
_MovFrOP1 | 4183 | 00:1B0C |
_NewLine | 452E | 01:5F4A |
_OneVar | 4BA3 | 3A:6420 |
_OP1ExOP2 | 421F | 00:1DD2 |
_OP1ExOP3 | 4219 | 00:1DB7 |
_OP1ExOP4 | 421C | 00:1DBC |
_OP1ExOP5 | 420D | 00:1DA0 |
_OP1ExOP6 | 4210 | 00:1DA5 |
_OP1ExpToDec | 4252 | 00:1E77 |
_OP1IntPartNeg | 489A | 04:74E8 |
_OP1Set0 | 41BF | 00:1BA4 |
_OP1Set1 | 419B | 00:1B38 |
_OP1Set2 | 41A7 | 00:1B50 |
_OP1Set3 | 41A1 | 00:1B44 |
_OP1Set4 | 419E | 00:1B3D |
_OP1ToOP2 | 412F | 00:1A2F |
_OP1ToOP3 | 4123 | 00:1A0F |
_OP1ToOP4 | 4117 | 00:19EC |
_OP1ToOP5 | 4153 | 00:1A80 |
_OP1ToOP6 | 4150 | 00:1A78 |
_OP2ExOP4 | 4213 | 00:1DAA |
_OP2ExOP5 | 4216 | 00:1DAF |
_OP2ExOP6 | 4207 | 00:1D93 |
_OP2Set0 | 41BC | 00:1B96 |
_OP2Set1 | 41AD | 00:1B60 |
_OP2Set2 | 41AA | 00:1B55 |
_OP2Set3 | 4198 | 00:1B30 |
_OP2Set4 | 4195 | 00:1B29 |
_OP2Set5 | 418F | 00:1B22 |
_OP2Set60 | 4AB0 | 38:5DDC |
_OP2Set8 | 418C | 00:1B1B |
_OP2SetA | 4192 | 00:1B24 |
_OP2ToOP1 | 4156 | 00:1A88 |
_OP2ToOP3 | 416E | 00:1AE7 |
_OP2ToOP4 | 411A | 00:19F5 |
_OP2ToOP5 | 414A | 00:1A68 |
_OP2ToOP6 | 414D | 00:1A70 |
_OP3Set0 | 41B9 | 00:1B8A |
_OP3Set1 | 4189 | 00:1B16 |
_OP3Set2 | 41A4 | 00:1B4B |
_OP3ToOP1 | 413E | 00:1A4E |
_OP3ToOP2 | 4120 | 00:1A07 |
_OP3ToOP4 | 4114 | 00:19E3 |
_OP3ToOP5 | 4147 | 00:1A60 |
_OP4Set0 | 41B6 | 00:1B85 |
_OP4Set1 | 4186 | 00:1B11 |
_OP4ToOP1 | 4138 | 00:1A44 |
_OP4ToOP2 | 411D | 00:19FE |
_OP4ToOP3 | 4171 | 00:1AEF |
_OP4ToOP5 | 4144 | 00:1A58 |
_OP4ToOP6 | 4177 | 00:1AF9 |
_OP5ExOP6 | 420A | 00:1D98 |
_OP5Set0 | 41B3 | 00:1B80 |
_OP5ToOP1 | 413B | 00:1A49 |
_OP5ToOP2 | 4126 | 00:1A17 |
_OP5ToOP3 | 4174 | 00:1AF4 |
_OP5ToOP4 | 412C | 00:1A27 |
_OP5ToOP6 | 4129 | 00:1A1F |
_OP6ToOP1 | 4135 | 00:1A3F |
_OP6ToOP2 | 4132 | 00:1A37 |
_OP6ToOP5 | 4141 | 00:1A53 |
_OutputExpr | 4BB2 | 03:4AF2 |
_PagedGet | 5023 | 00:17BB |
_ParseInp | 4A9B | 38:5987 |
_ParseInpLastEnt | 4B07 | 38:5984 |
_PDspGrph | 48A3 | 04:7904 |
_PixelTest | 48B5 | 04:79E7 |
_Plus1 | 4069 | 00:2285 |
_PointCmd | 48B2 | 04:79B2 |
_PointOn | 4C39 | 04:4155 |
_PopMCplxO1 | 436F | 00:14BC |
_PopOP1 | 437E | 00:14EA |
_PopOP3 | 437B | 00:14DA |
_PopOP5 | 4378 | 00:14CA |
_PopReal | 4393 | 00:1512 |
_PopRealO1 | 4390 | 00:150F |
_PopRealO2 | 438D | 00:150A |
_PopRealO3 | 438A | 00:1505 |
_PopRealO4 | 4387 | 00:1500 |
_PopRealO5 | 4384 | 00:14FB |
_PopRealO6 | 4381 | 00:14F6 |
_PosNo0Int | 422E | 00:1DF7 |
_PToR | 40F3 | 02:50BD |
_PushMCplxO1 | 43CF | 00:15A6 |
_PushMCplxO3 | 43C6 | 00:1594 |
_PushOP1 | 43C9 | 00:1599 |
_PushOP3 | 43C3 | 00:1581 |
_PushOP5 | 43C0 | 00:1573 |
_PushReal | 43BD | 00:155F |
_PushRealO1 | 43BA | 00:155C |
_PushRealO2 | 43B7 | 00:1554 |
_PushRealO3 | 43B4 | 00:154F |
_PushRealO4 | 43B1 | 00:154A |
_PushRealO5 | 43AE | 00:1545 |
_PushRealO6 | 43AB | 00:1540 |
_PushZeroOP1 | 4651 | 02:49C0 |
_PutAway | 4039 | 00:08AF |
_PutC | 4504 | 01:5B4C |
_PutMap | 4501 | 01:5A98 |
_PutPS | 4510 | 01:5C73 |
_PutPSB | 450D | 01:5C52 |
_PutS | 450A | 01:5C39 |
_PutTokString | 4960 | 06:46FD |
_PutToL | 4645 | 02:4829 |
_PutToMat | 461E | 02:406C |
_RandInit | 4B7F | 36:7E8A |
_Random | 4B79 | 36:7DC9 |
_Rcl_StatVar | 42DC | 00:2149 |
_RclAns | 4AD7 | 38:679F |
_RclCListElem | 464B | 02:49A7 |
_RclCListElemB | 464E | 02:49B5 |
_RclGDB2 | 47D1 | 33:72D9 |
_RclListElemB | 463C | 02:47FE |
_RclListElemToOP1 | 4639 | 02:47FB |
_RclN | 4ADD | 38:67A9 |
_RclSysTok | 4AE6 | 38:683E |
_RclVarPush | 4B9A | 3A:5D07 |
_RclVarSym | 4AE3 | 38:67B1 |
_RclX | 4AE0 | 38:67AE |
_RclY | 4ADA | 38:67A4 |
_Rec1stByte | 4EFA | 3C:439C |
_Rec1stByteNC | 4EFD | 3C:43A3 |
_RecAByteIO | 4F03 | 3C:443F |
_RedimMat | 4A26 | 07:4D3B |
_Regraph | 488E | 04:6764 |
_ReleaseBuffer | 4771 | 03:47AC |
_ReloadAppEntryVecs | 4C36 | 3B:73E4 |
_RestoreDisp | 4870 | 04:6176 |
_RName | 427F | 00:1F4C |
_RndGuard | 409F | 02:6A57 |
_RnFx | 40A2 | 02:6A71 |
_Round | 40A8 | 00:2623 |
_RToD | 4078 | 00:2374 |
_RToP | 40F6 | 02:50DB |
_RunIndicOff | 4570 | 01:6531 |
_RunIndicOn | 456D | 01:6518 |
_SaveDisp | 4C7B | 39:5DD8 |
_scr_4056 | 51F1 | 05:4056 |
_scr_4619 | 51E5 | 05:4619 |
_ScreenSplit | 5227 | 05:7712 |
_SendAByte | 4EE5 | 3C:420D |
_SendVarCmd | 4A14 | 3C:4EDD |
_SetAllPlots | 4FCC | 38:49C7 |
_SetExSpeed | 50BF | 00:0DCA |
_SetFuncM | 4840 | 36:7D11 |
_SetNorm_Vals | 49FC | 00:220F |
_SetParM | 4849 | 36:7D39 |
_SetPolM | 4846 | 36:7D2C |
_SetSeqM | 4843 | 36:7D1F |
_SetTblGraphDraw | 4C00 | 00:00F5 |
_SetupPagedPtr | 5020 | 00:17AC |
_SetXXOP1 | 478C | 33:5F7E |
_SetXXOP2 | 478F | 33:5F83 |
_SetXXXXOP2 | 4792 | 33:5F9E |
_SFont_Len | 4786 | 03:4ABD |
_Sin | 40BD | 02:7342 |
_SinCosRad | 40BA | 02:733E |
_SinH | 40CF | 02:7632 |
_SinHCosH | 40C6 | 02:7626 |
_SqRoot | 409C | 02:6E38 |
_SrchVLstDn | 4F12 | 07:71D7 |
_SrchVLstUp | 4F0F | 07:707F |
_SStringLength | 4CB4 | 3B:61A6 |
_sta_5d3c | 5203 | 35:5D3C |
_sta_5eef | 4B9D | 3A:5EEF |
_sta_760f | 4BA9 | 3A:760F |
_StMatEl | 4AE9 | 38:6C8F |
_StoAns | 4ABF | 38:6251 |
_StoGDB2 | 47CE | 33:71AC |
_StoN | 4ACB | 38:6274 |
_StoOther | 4AD4 | 38:62A9 |
_StoR | 4AC5 | 38:6264 |
_StoRand | 4B7C | 36:7E06 |
_StoSysTok | 4ABC | 38:623B |
_StoT | 4ACE | 38:629B |
_StoTheta | 4AC2 | 38:625C |
_StoX | 4AD1 | 38:62A3 |
_StoY | 4AC8 | 38:626C |
_StrCopy | 44E3 | 00:2810 |
_StrLength | 4C3F | 36:7F91 |
_Tan | 40C3 | 02:734A |
_TanH | 40C9 | 02:762A |
_TanLnF | 48BB | 04:7A43 |
_TenX | 40B7 | 02:7066 |
_ThetaName | 427C | 00:1F48 |
_ThreeExec | 4675 | 02:64ED |
_Times2 | 4066 | 00:2282 |
_TimesPt5 | 407E | 00:2382 |
_TName | 428E | 00:1F69 |
_ToFrac | 4657 | 02:4BBE |
_Trunc | 4060 | 00:2279 |
_UCLineS | 4795 | 33:6010 |
_UnLineCmd | 48AF | 04:797C |
_UnOPExec | 4672 | 02:5E14 |
_VertCmd | 48A9 | 04:7955 |
_VertSplitDraw | 48DC | 05:5D88 |
_VPutMap | 455E | 01:6293 |
_VPutS | 4561 | 01:646D |
_VPutSN | 4564 | 01:644D |
_VtoWHLDE | 47FB | 04:4410 |
_XftoI | 4804 | 37:41EB |
_Xitof | 47FE | 04:441E |
_XName | 4288 | 00:1F61 |
_XRootY | 479E | 33:632E |
_YftoI | 4801 | 37:41DF |
_YName | 428B | 00:1F65 |
_YToX | 47A1 | 33:6340 |
_Zero16D | 41B0 | 00:1B6F |
_ZeroOP | 41CE | 00:1BBC |
_ZeroOP1 | 41C5 | 00:1BAF |
_ZeroOP2 | 41C8 | 00:1BB4 |
_ZeroOP3 | 41CB | 00:1BB9 |
_ZmDecml | 484F | 36:7BA4 |
_ZmFit | 485B | 36:7A57 |
_ZmInt | 484C | 04:5F85 |
_ZmPrev | 4852 | 04:5FFE |
_ZmSquare | 485E | 36:7ABE |
_ZmStats | 47A4 | 33:65DC |
_ZmTrig | 4861 | 36:7B36 |
_ZmUsr | 4855 | 04:601D |
_ZooDefault | 4867 | 36:7BF9 |
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.
| bcall | ID | Body (page:addr) |
|---|---|---|
_MD5Final | 8018 | 3F:6964 |
_RSAValidate | 801B | 3F:6CB4 |
_cmpStr | 801E | 3F:7195 |
_WriteAByte | 8021 | 3F:4C9F |
_EraseFlash | 8024 | 3F:4C2A |
_FindFirstCertField | 8027 | 3F:4D62 |
_ZeroToCertificate | 802A | 3F:4DAF |
_GetCertificateEnd | 802D | 3F:4D53 |
_FindGroupedField | 8030 | 3F:4E8C |
_ret_1 | 8033 | 3F:4867 |
_ret_2 | 8036 | 3F:4867 |
_ret_3 | 8039 | 3F:4867 |
_ret_4 | 803C | 3F:4867 |
_ret_5 | 803F | 3F:4867 |
_Mult8By8 | 8042 | 3F:7059 |
_Mult16By8 | 8045 | 3F:705B |
_Div16By8 | 8048 | 3F:7146 |
_Div16By16 | 804B | 3F:7148 |
_LoadAIndPaged | 8051 | 3F:486E |
_FlashToRam2 | 8054 | 3F:4888 |
_GetCertificateStart | 8057 | 3F:4D46 |
_GetFieldSize | 805A | 3F:4DB8 |
_FindSubField | 805D | 3F:4DFB |
_EraseCertificateSector | 8060 | 3F:4E3F |
_CheckHeaderKey | 8063 | 3F:4B4A |
_Load_LFontV2 | 806C | 3F:7C8A |
_Load_LFontV | 806F | 3F:7C8A |
_ReceiveOS | 8072 | 3F:5DCE |
_FindOSHeaderSubField | 8075 | 3F:5018 |
_FindNextCertField | 8078 | 3F:4D5C |
_GetByteOrBoot | 807B | 3F:5C64 |
_getSerial | 807E | 3F:442F |
_ReceiveCalcID | 8081 | 3F:5EDC |
_EraseFlashPage | 8084 | 3F:4C1E |
_WriteFlashUnsafe | 8087 | 3F:4CA6 |
_dispBootVer | 808A | 3F:44F1 |
_MD5Init | 808D | 3F:68ED |
_MD5Update | 8090 | 3F:6907 |
_MarkOSInvalid | 8093 | 3F:5209 |
_FindProgramLicense | 8096 | 3F:4B1A |
_MarkOSValid | 8099 | 3F:51F5 |
_CheckOSValidated | 809C | 3F:52C6 |
_SetupAppPubKey | 809F | 3F:53CA |
_SigModR | 80A2 | 3F:7225 |
_TransformHash | 80A5 | 3F:723F |
_IsAppFreeware | 80A8 | 3F:52E1 |
_FindAppHeaderSubField | 80AB | 3F:500A |
_WriteValidationNumber | 80AE | 3F:540B |
_Div32By16 | 80B1 | 3F:706E |
_FindGroup | 80B4 | 3F:4E61 |
_getBootVer | 80B7 | 3F:477C |
_getHardwareVersion | 80BA | 3F:4781 |
_xorA | 80BD | 3F:5C6D |
_bignumpowermod17 | 80C0 | 3F:6CBD |
_ProdNrPart1 | 80C3 | 3F:6209 |
_WriteAByteSafe | 80C6 | 3F:4C9A |
_WriteFlash | 80C9 | 3F:4C8F |
_SetupDateStampPubKey | 80CC | 3F:548C |
_SetFlashLowerBound | 80CF | 3F:4784 |
_LowBatteryBoot | 80D2 | 3F:5834 |
_AttemptUSBOSReceive | 80E4 | 2F:4145 |
_DisplayBootMessage | 80E7 | 3F:6127 |
_NewLine2 | 80EA | 3F:73DD |
_DisplayBootError10 | 80ED | 3F:5789 |
_Chk_Batt_Low_B | 80F0 | 3F:6171 |
_Chk_Batt_Low_B2 | 80F3 | 3F:6163 |
_ReceiveOS_USB | 80F6 | 2F:48CA |
_DisplayOSProgress | 80F9 | 3F:62D0 |
_ResetCalc | 80FC | 3F:5ED3 |
_SetupOSPubKey | 80FF | 3F:5387 |
_CheckHeaderKeyHL | 8102 | 3F:4B4D |
_USBErrorCleanup | 8105 | 2F:5958 |
_InitUSB | 8108 | 2F:52A4 |
_KillUSB | 810E | 2F:5961 |
_DisplayBootError1 | 8111 | 3F:63DB |
_DisplayBootError2 | 8114 | 3F:5789 |
_DisplayBootError3 | 8117 | 3F:5789 |
_DisplayBootError4 | 811A | 3F:5789 |
_DisplayBootError5 | 811D | 3F:5789 |
_DisplayBootError6 | 8120 | 3F:5789 |
_DisplayBootError7 | 8123 | 3F:5789 |
_DisplayBootError8 | 8126 | 3F:5789 |
_DisplayBootError9 | 8129 | 3F:5789 |
2-byte token tables
The second-byte tables for every 2-byte token on the TI-84 Plus (OS 2.55MP). The tokenizer detects a 2-byte lead via _IsA2ByteTok (00:1FE8) — see Tokenizer & TI-BASIC — then the second byte indexes the group below.
Names are sourced from the TI-Toolkit/tokens sheet (8X.xml) and filtered to tokens that exist on the monochrome 84+ (≤ 2.55MP): the TI-84+CSE/TI-84+CE color/Python-era 2-byte tokens are excluded. The Since column is the model that introduced each token. Token text is the English display form (the source also carries an ASCII-accessible spelling, e.g. >DMS for ►DMS).
492 two-byte tokens across 11 lead bytes. (Single-byte tokens are modeled by the TIToken enum — see 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:
| 2nd | Token | Since |
|---|---|---|
00 | [A] | TI-82 |
01 | [B] | TI-82 |
02 | [C] | TI-82 |
03 | [D] | TI-82 |
04 | [E] | TI-82 |
05 | [F] | TI-83 |
06 | [G] | TI-83 |
07 | [H] | TI-83 |
08 | [I] | TI-83 |
09 | [J] | TI-83 |
5D — List names (built-in L₁–L₆)
6 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | L₁ | TI-82 |
01 | L₂ | TI-82 |
02 | L₃ | TI-82 |
03 | L₄ | TI-82 |
04 | L₅ | TI-82 |
05 | L₆ | TI-82 |
5E — Equation variables (Y= functions, parametric, polar, sequence)
31 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
10 | Y₁ | TI-82 |
11 | Y₂ | TI-82 |
12 | Y₃ | TI-82 |
13 | Y₄ | TI-82 |
14 | Y₅ | TI-82 |
15 | Y₆ | TI-82 |
16 | Y₇ | TI-82 |
17 | Y₈ | TI-82 |
18 | Y₉ | TI-82 |
19 | Y₀ | TI-82 |
20 | X₁ᴛ | TI-82 |
21 | Y₁ᴛ | TI-82 |
22 | X₂ᴛ | TI-82 |
23 | Y₂ᴛ | TI-82 |
24 | X₃ᴛ | TI-82 |
25 | Y₃ᴛ | TI-82 |
26 | X₄ᴛ | TI-82 |
27 | Y₄ᴛ | TI-82 |
28 | X₅ᴛ | TI-82 |
29 | Y₅ᴛ | TI-82 |
2A | X₆ᴛ | TI-82 |
2B | Y₆ᴛ | TI-82 |
40 | r₁ | TI-82 |
41 | r₂ | TI-82 |
42 | r₃ | TI-82 |
43 | r₄ | TI-82 |
44 | r₅ | TI-82 |
45 | r₆ | TI-82 |
80 | u | TI-82 |
81 | v | TI-82 |
82 | w | TI-82 |
60 — Pictures (Pic1–Pic0)
10 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | Pic1 | TI-82 |
01 | Pic2 | TI-82 |
02 | Pic3 | TI-82 |
03 | Pic4 | TI-82 |
04 | Pic5 | TI-82 |
05 | Pic6 | TI-82 |
06 | Pic7 | TI-83 |
07 | Pic8 | TI-83 |
08 | Pic9 | TI-83 |
09 | Pic0 | TI-83 |
61 — Graph databases (GDB1–GDB0)
10 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | GDB1 | TI-82 |
01 | GDB2 | TI-82 |
02 | GDB3 | TI-82 |
03 | GDB4 | TI-82 |
04 | GDB5 | TI-82 |
05 | GDB6 | TI-82 |
06 | GDB7 | TI-83 |
07 | GDB8 | TI-83 |
08 | GDB9 | TI-83 |
09 | GDB0 | TI-83 |
62 — Statistics / regression / output variables
60 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
01 | RegEQ | TI-82 |
02 | n | TI-82 |
03 | x̄ | TI-82 |
04 | Σx | TI-82 |
05 | Σx² | TI-82 |
06 | Sx | TI-82 |
07 | σx | TI-82 |
08 | minX | TI-82 |
09 | maxX | TI-82 |
0A | minY | TI-82 |
0B | maxY | TI-82 |
0C | ȳ | TI-82 |
0D | Σy | TI-82 |
0E | Σy² | TI-82 |
0F | Sy | TI-82 |
10 | σy | TI-82 |
11 | Σxy | TI-82 |
12 | r | TI-82 |
13 | Med | TI-82 |
14 | Q₁ | TI-82 |
15 | Q₃ | TI-82 |
16 | a | TI-82 |
17 | b | TI-82 |
18 | c | TI-82 |
19 | d | TI-82 |
1A | e | TI-82 |
1B | x₁ | TI-82 |
1C | x₂ | TI-82 |
1D | x₃ | TI-82 |
1E | y₁ | TI-82 |
1F | y₂ | TI-82 |
20 | y₃ | TI-82 |
21 | 𝑛 | TI-82 |
22 | p | TI-82 |
23 | z | TI-82 |
24 | t | TI-82 |
25 | χ² | TI-82 |
26 | 𝙵 | TI-82 |
27 | df | TI-82 |
28 | p̂ | TI-82 |
29 | p̂₁ | TI-82 |
2A | p̂₂ | TI-82 |
2B | x̄₁ | TI-82 |
2C | Sx₁ | TI-82 |
2D | n₁ | TI-82 |
2E | x̄₂ | TI-82 |
2F | Sx₂ | TI-82 |
30 | n₂ | TI-82 |
31 | Sxp | TI-82 |
32 | lower | TI-82 |
33 | upper | TI-82 |
34 | s | TI-82 |
35 | r² | TI-82 |
36 | R² | TI-82 |
37 | df | TI-82 |
38 | SS | TI-82 |
39 | MS | TI-82 |
3A | df | TI-82 |
3B | SS | TI-82 |
3C | MS | TI-82 |
63 — Window & system variables
56 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | ZXscl | TI-82 |
01 | ZYscl | TI-82 |
02 | Xscl | TI-82 |
03 | Yscl | TI-82 |
04 | UnStart | TI-82 |
05 | VnStart | TI-82 |
06 | U𝑛-₁ | TI-82 |
07 | V𝑛-₁ | TI-82 |
08 | ZUnStart | TI-82 |
09 | ZVnStart | TI-82 |
0A | Xmin | TI-82 |
0B | Xmax | TI-82 |
0C | Ymin | TI-82 |
0D | Ymax | TI-82 |
0E | Tmin | TI-82 |
0F | Tmax | TI-82 |
10 | θmin | TI-82 |
11 | θmax | TI-82 |
12 | ZXmin | TI-82 |
13 | ZXmax | TI-82 |
14 | ZYmin | TI-82 |
15 | ZYmax | TI-82 |
16 | Zθmin | TI-82 |
17 | Zθmax | TI-82 |
18 | ZTmin | TI-82 |
19 | ZTmax | TI-82 |
1A | TblStart | TI-82 |
1B | 𝑛Min | TI-82 |
1C | ZPlotStart | TI-82 |
1D | 𝑛Max | TI-82 |
1E | Z𝑛Max | TI-82 |
1F | 𝑛Start | TI-82 |
20 | Z𝑛Min | TI-82 |
21 | ΔTbl | TI-82 |
22 | Tstep | TI-82 |
23 | θstep | TI-82 |
24 | ZTstep | TI-82 |
25 | Zθstep | TI-82 |
26 | ΔX | TI-82 |
27 | ΔY | TI-82 |
28 | XFact | TI-82 |
29 | YFact | TI-82 |
2A | TblInput | TI-82 |
2B | 𝗡 | TI-83 |
2C | I% | TI-83 |
2D | PV | TI-83 |
2E | PMT | TI-83 |
2F | FV | TI-83 |
30 | P/Y | TI-83 |
31 | C/Y | TI-83 |
32 | w(𝑛Min) | TI-83 |
33 | Zw(𝑛Min) | TI-83 |
34 | PlotStep | TI-83 |
35 | ZPlotStep | TI-83 |
36 | Xres | TI-83 |
37 | ZXres | TI-83 |
7E — Graph-format & mode tokens
19 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | Sequential | TI-82 |
01 | Simul | TI-82 |
02 | PolarGC | TI-82 |
03 | RectGC | TI-82 |
04 | CoordOn | TI-82 |
05 | CoordOff | TI-82 |
06 | Connected | TI-82 |
07 | Dot | TI-82 |
08 | AxesOn | TI-82 |
09 | AxesOff | TI-82 |
0A | GridOn | TI-82 |
0B | GridOff | TI-82 |
0C | LabelOn | TI-82 |
0D | LabelOff | TI-82 |
0E | Web | TI-82 |
0F | Time | TI-82 |
10 | uvAxes | TI-82 |
11 | vwAxes | TI-82 |
12 | uwAxes | TI-82 |
AA — String variables (Str1–Str0)
10 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | Str1 | TI-83 |
01 | Str2 | TI-83 |
02 | Str3 | TI-83 |
03 | Str4 | TI-83 |
04 | Str5 | TI-83 |
05 | Str6 | TI-83 |
06 | Str7 | TI-83 |
07 | Str8 | TI-83 |
08 | Str9 | TI-83 |
09 | Str0 | TI-83 |
BB — Extended command page (2-byte commands)
232 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | npv( | TI-83 |
01 | irr( | TI-83 |
02 | bal( | TI-83 |
03 | ΣPrn( | TI-83 |
04 | ΣInt( | TI-83 |
05 | ►Nom( | TI-83 |
06 | ►Eff( | TI-83 |
07 | dbd( | TI-83 |
08 | lcm( | TI-83 |
09 | gcd( | TI-83 |
0A | randInt( | TI-83 |
0B | randBin( | TI-83 |
0C | sub( | TI-83 |
0D | stdDev( | TI-83 |
0E | variance( | TI-83 |
0F | inString( | TI-83 |
10 | normalcdf( | TI-83 |
11 | invNorm( | TI-83 |
12 | tcdf( | TI-83 |
13 | χ²cdf( | TI-83 |
14 | 𝙵cdf( | TI-83 |
15 | binompdf( | TI-83 |
16 | binomcdf( | TI-83 |
17 | poissonpdf( | TI-83 |
18 | poissoncdf( | TI-83 |
19 | geometpdf( | TI-83 |
1A | geometcdf( | TI-83 |
1B | normalpdf( | TI-83 |
1C | tpdf( | TI-83 |
1D | χ²pdf( | TI-83 |
1E | 𝙵pdf( | TI-83 |
1F | randNorm( | TI-83 |
20 | tvm_Pmt | TI-83 |
21 | tvm_I% | TI-83 |
22 | tvm_PV | TI-83 |
23 | tvm_𝗡 | TI-83 |
24 | tvm_FV | TI-83 |
25 | conj( | TI-83 |
26 | real( | TI-83 |
27 | imag( | TI-83 |
28 | angle( | TI-83 |
29 | cumSum( | TI-83 |
2A | expr( | TI-83 |
2B | length( | TI-83 |
2C | ΔList( | TI-83 |
2D | ref( | TI-83 |
2E | rref( | TI-83 |
2F | ►Rect | TI-83 |
30 | ►Polar | TI-83 |
31 | 𝑒 | TI-83 |
32 | SinReg | TI-83 |
33 | Logistic | TI-83 |
34 | LinRegTTest | TI-83 |
35 | ShadeNorm( | TI-83 |
36 | Shade_t( | TI-83 |
37 | Shadeχ²( | TI-83 |
38 | Shade𝙵( | TI-83 |
39 | Matr►list( | TI-83 |
3A | List►matr( | TI-83 |
3B | Z-Test( | TI-83 |
3C | T-Test | TI-83 |
3D | 2-SampZTest( | TI-83 |
3E | 1-PropZTest( | TI-83 |
3F | 2-PropZTest( | TI-83 |
40 | χ²-Test( | TI-83 |
41 | ZInterval | TI-83 |
42 | 2-SampZInt( | TI-83 |
43 | 1-PropZInt( | TI-83 |
44 | 2-PropZInt( | TI-83 |
45 | GraphStyle( | TI-83 |
46 | 2-SampTTest | TI-83 |
47 | 2-Samp𝙵Test | TI-83 |
48 | TInterval | TI-83 |
49 | 2-SampTInt | TI-83 |
4A | SetUpEditor | TI-83 |
4B | Pmt_End | TI-83 |
4C | Pmt_Bgn | TI-83 |
4D | Real | TI-83 |
4E | r𝑒^θ𝑖 | TI-83 |
4F | a+b𝑖 | TI-83 |
50 | ExprOn | TI-83 |
51 | ExprOff | TI-83 |
52 | ClrAllLists | TI-83 |
53 | GetCalc( | TI-83 |
54 | DelVar | TI-83 |
55 | Equ►String( | TI-83 |
56 | String►Equ( | TI-83 |
57 | Clear Entries | TI-83 |
58 | Select( | TI-83 |
59 | ANOVA( | TI-83 |
5A | ModBoxplot | TI-83 |
5B | NormProbPlot | TI-83 |
64 | G-T | TI-83 |
65 | ZoomFit | TI-83 |
66 | DiagnosticOn | TI-83 |
67 | DiagnosticOff | TI-83 |
68 | Archive | TI-83+ |
69 | UnArchive | TI-83+ |
6A | Asm( | TI-83+ |
6B | AsmComp( | TI-83+ |
6C | AsmPrgm | TI-83+ |
6E | Á | TI-83+ |
6F | À | TI-83+ |
70 | Â | TI-83+ |
71 | Ä | TI-83+ |
72 | á | TI-83+ |
73 | à | TI-83+ |
74 | â | TI-83+ |
75 | ä | TI-83+ |
76 | É | TI-83+ |
77 | È | TI-83+ |
78 | Ê | TI-83+ |
79 | Ë | TI-83+ |
7A | é | TI-83+ |
7B | è | TI-83+ |
7C | ê | TI-83+ |
7D | ë | TI-83+ |
7F | Ì | TI-83+ |
80 | Î | TI-83+ |
81 | Ï | TI-83+ |
82 | í | TI-83+ |
83 | ì | TI-83+ |
84 | î | TI-83+ |
85 | ï | TI-83+ |
86 | Ó | TI-83+ |
87 | Ò | TI-83+ |
88 | Ô | TI-83+ |
89 | Ö | TI-83+ |
8A | ó | TI-83+ |
8B | ò | TI-83+ |
8C | ô | TI-83+ |
8D | ö | TI-83+ |
8E | Ú | TI-83+ |
8F | Ù | TI-83+ |
90 | Û | TI-83+ |
91 | Ü | TI-83+ |
92 | ú | TI-83+ |
93 | ù | TI-83+ |
94 | û | TI-83+ |
95 | ü | TI-83+ |
96 | Ç | TI-83+ |
97 | ç | TI-83+ |
98 | Ñ | TI-83+ |
99 | ñ | TI-83+ |
9A | ´ | TI-83+ |
9B | ` | TI-83+ |
9C | ¨ | TI-83+ |
9D | ¿ | TI-83+ |
9E | ¡ | TI-83+ |
9F | α | TI-83+ |
A0 | β | TI-83+ |
A1 | γ | TI-83+ |
A2 | Δ | TI-83+ |
A3 | δ | TI-83+ |
A4 | ε | TI-83+ |
A5 | λ | TI-83+ |
A6 | μ | TI-83+ |
A7 | π | TI-83+ |
A8 | ρ | TI-83+ |
A9 | Σ | TI-83+ |
AB | Φ | TI-83+ |
AC | Ω | TI-83+ |
AD | p̂ | TI-83+ |
AE | χ | TI-83+ |
AF | 𝙵 | TI-83+ |
B0 | a | TI-83+ |
B1 | b | TI-83+ |
B2 | c | TI-83+ |
B3 | d | TI-83+ |
B4 | e | TI-83+ |
B5 | f | TI-83+ |
B6 | g | TI-83+ |
B7 | h | TI-83+ |
B8 | i | TI-83+ |
B9 | j | TI-83+ |
BA | k | TI-83+ |
BC | l | TI-83+ |
BD | m | TI-83+ |
BE | n | TI-83+ |
BF | o | TI-83+ |
C0 | p | TI-83+ |
C1 | q | TI-83+ |
C2 | r | TI-83+ |
C3 | s | TI-83+ |
C4 | t | TI-83+ |
C5 | u | TI-83+ |
C6 | v | TI-83+ |
C7 | w | TI-83+ |
C8 | x | TI-83+ |
C9 | y | TI-83+ |
CA | z | TI-83+ |
CB | σ | TI-83+ |
CC | τ | TI-83+ |
CD | Í | TI-83+ |
CE | GarbageCollect | TI-83+ |
CF | ~ | TI-83+ |
D1 | @ | TI-83+ |
D2 | # | TI-83+ |
D3 | $ | TI-83+ |
D4 | & | TI-83+ |
D5 | ` | TI-83+ |
D6 | ; | TI-83+ |
D7 | \ | TI-83+ |
D8 | | | TI-83+ |
D9 | _ | TI-83+ |
DA | % | TI-83+ |
DB | … | TI-83+ |
DC | ∠ | TI-83+ |
DD | ß | TI-83+ |
DE | ˣ | TI-83+ |
DF | ᴛ | TI-83+ |
E0 | ₀ | TI-83+ |
E1 | ₁ | TI-83+ |
E2 | ₂ | TI-83+ |
E3 | ₃ | TI-83+ |
E4 | ₄ | TI-83+ |
E5 | ₅ | TI-83+ |
E6 | ₆ | TI-83+ |
E7 | ₇ | TI-83+ |
E8 | ₈ | TI-83+ |
E9 | ₉ | TI-83+ |
EA | ₁₀ | TI-83+ |
EB | ◄ | TI-83+ |
EC | ► | TI-83+ |
ED | ↑ | TI-83+ |
EE | ↓ | TI-83+ |
F0 | × | TI-83+ |
F1 | ∫ | TI-83+ |
F2 | 🡁 | TI-83+ |
F3 | 🠿 | TI-83+ |
F4 | √ | TI-83+ |
F5 | ⌸ | TI-83+ |
EF — TI-84+-era extended tokens (date/time, clock, GarbageCollect…)
48 tokens on the 84+ (2.55MP). Second byte → token:
| 2nd | Token | Since |
|---|---|---|
00 | setDate( | TI-84+ |
01 | setTime( | TI-84+ |
02 | checkTmr( | TI-84+ |
03 | setDtFmt( | TI-84+ |
04 | setTmFmt( | TI-84+ |
05 | timeCnv( | TI-84+ |
06 | dayOfWk( | TI-84+ |
07 | getDtStr( | TI-84+ |
08 | getTmStr( | TI-84+ |
09 | getDate | TI-84+ |
0A | getTime | TI-84+ |
0B | startTmr | TI-84+ |
0C | getDtFmt | TI-84+ |
0D | getTmFmt | TI-84+ |
0E | isClockOn | TI-84+ |
0F | ClockOff | TI-84+ |
10 | ClockOn | TI-84+ |
11 | OpenLib( | TI-84+ |
12 | ExecLib | TI-84+ |
13 | invT( | TI-84+ |
14 | χ²GOF-Test( | TI-84+ |
15 | LinRegTInt | TI-84+ |
16 | Manual-Fit | TI-84+ |
17 | ZQuadrant1 | TI-84+ |
18 | ZFrac1⁄2 | TI-84+ |
19 | ZFrac1⁄3 | TI-84+ |
1A | ZFrac1⁄4 | TI-84+ |
1B | ZFrac1⁄5 | TI-84+ |
1C | ZFrac1⁄8 | TI-84+ |
1D | ZFrac1⁄10 | TI-84+ |
1E | ⬚ | TI-84+ |
2E | ⁄ | TI-84+ |
2F | | TI-84+ |
30 | ►n⁄d◄►Un⁄d | TI-84+ |
31 | ►F◄►D | TI-84+ |
32 | remainder( | TI-84+ |
33 | Σ( | TI-84+ |
34 | logBASE( | TI-84+ |
35 | randIntNoRep( | TI-84+ |
37 | MATHPRINT | TI-84+ |
38 | CLASSIC | TI-84+ |
39 | n⁄d | TI-84+ |
3A | Un⁄d | TI-84+ |
3B | AUTO | TI-84+ |
3C | DEC | TI-84+ |
3D | FRAC | TI-84+ |
3F | STATWIZARD ON | TI-84+ |
40 | STATWIZARD OFF | TI-84+ |
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
- 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, andflash_op_fd/fb/fe@3D:7C8F/7C93/7C97. The public bcall entry points for these primitives are named inti83plus.inc:_WriteAByte(8021),_WriteAByteSafe(80C6), and_FlashToRam2(8054); the retail boot table maps them to3F:4C9F,3F:4C9A, and3F:4888. The candidate addresses3D:61AF,3D:64AA,3D:62C2,3D:6413, and3D:6B9Bare still undisassembled (not defined functions) in the live DB; theirflash_*names are project-local inferred labels, not WikiTI orti83plus.incequates. See sub-vat-archive.md / 12. - Flash archive garbage collector — the behavior is documented. The GC-path candidates
flash_gc_relocate@3C:7BD0,gc_show_screen@3C:7E0D, andflash_cmd_dispatch@3C:7121are still undisassembled (not defined functions) in the live DB; thoseflash_*/gc_*names are project-local inferred labels, not WikiTI orti83plus.incequates. - Enum equates. Apply
TIKeyCode/TIError/TIVarTypeto scalar operands in the relevant handlers (conservative, scoped). - 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(/AsmPrgmcompile/setup body before the traced payload handoff (ram:9D95 op=0xC9is confirmed), direct ASM-initiated BASIC program execution beyond VAT lookup and cooperativeAnscallback (_ChkFindSymworks fromAsmPrgm; theASMFORM.8xp/ZZFORM.8xp_Find_Parse_Formulafixture fails withERR:UNDEFINED; theASMPARSE.8xp/ZZPARSE.8xp_ParseInpLastEntfixture fails withERR:INVALID;_ExecuteNewPrgmreachesERR:SYNTAX;_JForceCmdabandons the caller stack;_PutTokString/_rclToQueueare edit-buffer paths, not proven program-call entries), and the group-archive member walk (_Arc_Unarc’sCP 0x17reject 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).