Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The bcall mechanism

bcall is how the OS spans 1 MiB with a 64 KiB CPU: a routine on any page calls a routine on any other page without knowing where it physically lives.

The call site

RST 28h          ; opcode 0xEF
.dw  <bcall_id>   ; 2-byte little-endian ID immediately after

rst 28h is a 1-byte Z80 call 0028h. So the return address pushed on the stack points at the 2-byte ID. The dispatcher reads the ID through the return address, then fixes the return to skip those 2 bytes — i.e. execution resumes at call_site + 3. [confirmed] (modeled in Ghidra by setting each rst 28h’s fall-through to +3 and typing the ID as a word.)

The dispatcher — bcall_dispatcher @ ram:2a2f [confirmed]

From the decompiler:

  1. Read the 2-byte ID dw from the caller’s return address.
  2. Decode the ID’s high bits: bit15/bit14 select the address class; the low bits form the table offset.
  3. Bank the bcall table page into slot A (via the helper at ram:181c, which sets port_mapBankA).
  4. Read the 3-byte table entry: target address (2) + target page (1).
  5. Bank the target page into slot A (port_mapBankA = page), save the previous page.
  6. call the target. On return, restore the previous page and resume the caller at +3.

The jump table — flash page 0x3B [confirmed]

  • Located at the start of physical flash page 0x3B (file offset 0x3B*0x4000 = 0xEC000).
  • 3-byte entries: addr_lo, addr_hi, page. IDs step by 3 from 0x4000, so entry for ID X is at table offset X-0x4000.
  • Resolution method (tools/ Python): scored all 64 pages by how many named IDs produced a valid (addr∈4000..7FFF or page-0, page<0x40) entry; page 0x3B scored highest (the page-selection heuristic uses a conservative validity filter chosen only to pick the table). Once 0x3B is selected and applied, 645 entries resolve and are live-confirmed. Of those IDs, 623 also appear in the included SDK equates and 22 are project-inferred additions.
  • Validation: known bcalls land exactly where expected — _PutS01:5C39, _GetKey06:491E, _ClrLCDFull01:60E4, _GetCSC00:04B2, _CreateReal00:10B8.

tools/symbols/bcall_targets.txt holds 645 resolved main-table bcall rows. The retail boot table has 87 populated entries. tools/symbols/bcalls8x_targets.txt holds the 83 rows with official SDK names; four populated slots have only project-inferred names in the bcall index. tools/ti84re/rom/resolve_bcalls.py emits the official-name rows only when page 3F has the retail prefix and page 2F contains the companion USB payload; its BootFree guard otherwise leaves only diagnostic comments. tools/ghidra/ApplyBcalls.java disassembles and names the confirmed bodies. tools/ghidra/BcallEvidenceStudy.java then provides a read-only listing, reference, and decompiler dump for a selected set of IDs. For example:

nix develop -c ghidra-analyzeHeadless "$PWD" ti84 \
  -process -noanalysis -readOnly -scriptPath tools/ghidra \
  -postScript BcallEvidenceStudy.java tools/symbols/bcall_targets.txt \
  /tmp/bcall-evidence.txt 4030 4ED6 50C8

Jump-table ID ranges

The dispatcher (bcall_dispatcher) decodes the ID’s top two bits to pick the table page: bit 15 set → page-byte 0x7F (masked & 0x3F → page 0x3F); bit 14 set → 0x7B (→ page 0x3B); with neither bit set it falls through to lookup_bcall_table_page (ram:2ADA). The two tables real bcall IDs use:

  • 0x4xxx0x7FFF (bit 14 set): the main table on flash page 0x3B, entry at offset ID − 0x4000 (645 live-confirmed bcalls: 623 IDs also present in the included SDK equates and 22 project-inferred additions).
  • 0x8xxx (bit 15 set): the retail boot table is on physical page 3F, indexed by ID & 0x7FFF. Its real entries occupy IDs 0x80180x80D2 and 0x80E40x8129; bytes 3F:40D53F:40E3 between those ranges are executable dispatch-stub bytes, not five table entries. D84PBE1.8Xv supplies the retail page 3F; D84PBE2.8Xv supplies the companion USB boot support page 2F. Most entries resolve to 3F:addr; USB entries such as _AttemptUSBOSReceive (80E4) and _InitUSB (8108) resolve to 2F:addr. tools/ti84re/rom/resolve_bcalls.py refuses to emit these targets from a BootFree-substituted page. [confirmed]

Both resolved table formats are 3-byte entries: target address (little endian) plus page byte masked with & 0x3F.

RST shortcuts (fast inlined bcalls) [confirmed]

Five of the RST vectors are 1-byte fast paths for the hottest routines (each JPs to its page-0 handler, which is also reachable as a bcall — the table maps the same address). rst 28h is the bcall dispatcher itself (not a shortcut, and ram:2A2F is not a bcall target); it is listed here only to complete the vector set:

OpcodeVector → targetRoutine
rst 08h0008→1A2F_OP1ToOP2 (copy FP reg)
rst 10h0010→0E65_FindSym (VAT lookup)
rst 18h0018→155C_PushRealO1 (push OP1 to FPS)
rst 20h0020→1B01_Mov9ToOP1 (copy 9 bytes → OP1)
rst 28h0028→2A2Fbcall dispatcher
rst 30h0030→229E_FPAdd (float add)

All six match the documented TI-83+/84+ RST assignments — strong cross-confirmation of the table resolution.

bjump — the sibling mechanism (OS-internal cross-page calls)

Besides bcalls, the OS calls its own cross-page routines via bjump. Its encoding is:

CALL cross_page_jump ; = CALL ram:2B09
.dw addr
.db page

cross_page_jump reads the stacked return address (its caller’s, via an SP-relative load — it does not POP it), fetches the 2-byte target + 1-byte page from the inline descriptor there, rewrites the return frame past those 3 bytes, banks the page (& 0x3F), and returns into the target. The target’s RET then returns to the bjump’s caller, so it behaves like a call that consumes the 3 inline bytes.

There is a trampoline table in the page-0 address range ram:3B01ram:3D0A: 87 packed 6-byte entries, each a bjump to a hot OS routine on another page (ram:3D0B already begins a separate CALL ram:2B49 table). The static Ghidra database models it in the page-0/ROM address space; whether the table is copied to RAM at runtime remains a hypothesis. Code invokes a routine by CALL ram:3Bxx into the table. tools/symbols/bjumps.txt lists every entry’s (offset → page:addr); tools/ghidra/RamRoutines.java marks the inline .dw/.db as data and comments each target.

Example: _PutMap’s glyph blitter is reached via the trampoline at ram:3B3D → 07:4588.

Inline bjumps. Besides this trampoline table, the three-line bjump encoding above appears in packed dispatch tables and inside OS routines. The target returns to the bjump caller, so cross_page_jump consumes the three inline descriptor bytes as a non-returning tail-jump. tools/ghidra/FixInlineBjumps.java runs before and after the scripts that seed parser handlers and reviewed function entries. Each pass marks every disassembled inline site, including the 87 trampoline-table entries. Raw byte matches outside disassembled code are not counted. [confirmed]

Limitations

  • Keep the BootFree guard in place when regenerating from emulator-derived ROM images.
  • Some bcalls are thunks: e.g. _FindSym’s page-0 entry uses cross_page_jump to reach the real body on page 0x07.