The bcall mechanism
bcall is how the OS spans 1 MiB with a 64 KiB CPU: a routine on any page calls a routine on any other page without knowing where it physically lives.
The call site
RST 28h ; opcode 0xEF
.dw <bcall_id> ; 2-byte little-endian ID immediately after
rst 28h is a 1-byte Z80 call 0028h. So the return address pushed on the stack points at the 2-byte ID. The dispatcher reads the ID through the return address, then fixes the return to skip those 2 bytes — i.e. execution resumes at call_site + 3. [confirmed] (modeled in Ghidra by setting each rst 28h’s fall-through to +3 and typing the ID as a word.)
The dispatcher — bcall_dispatcher @ ram:2a2f [confirmed]
From the decompiler:
- Read the 2-byte ID
dwfrom the caller’s return address. - Decode the ID’s high bits:
bit15/bit14select the address class; the low bits form the table offset. - Bank the bcall table page into slot A (via the helper at
ram:181c, which setsport_mapBankA). - Read the 3-byte table entry: target address (2) + target page (1).
- Bank the target page into slot A (
port_mapBankA = page), save the previous page. callthe target. On return, restore the previous page and resume the caller at+3.
The jump table — flash page 0x3B [confirmed]
- Located at the start of physical flash page
0x3B(file offset0x3B*0x4000 = 0xEC000). - 3-byte entries:
addr_lo, addr_hi, page. IDs step by 3 from0x4000, so entry for ID X is at table offsetX-0x4000. - Resolution method (
tools/Python): scored all 64 pages by how many named IDs produced a valid(addr∈4000..7FFF or page-0, page<0x40)entry; page0x3Bscored highest (the page-selection heuristic uses a conservative validity filter chosen only to pick the table). Once0x3Bis selected and applied, 645 entries resolve and are live-confirmed. Of those IDs, 623 also appear in the included SDK equates and 22 are project-inferred additions. - Validation: known bcalls land exactly where expected —
_PutS→01:5C39,_GetKey→06:491E,_ClrLCDFull→01:60E4,_GetCSC→00:04B2,_CreateReal→00:10B8.
tools/symbols/bcall_targets.txt holds 645 resolved main-table bcall rows.
The retail boot table has 87 populated entries. tools/symbols/bcalls8x_targets.txt
holds the 83 rows with official SDK names; four populated slots have only
project-inferred names in the bcall index.
tools/ti84re/rom/resolve_bcalls.py emits the official-name rows only when page 3F has
the retail prefix and page 2F contains the companion USB payload; its
BootFree guard otherwise leaves only diagnostic comments.
tools/ghidra/ApplyBcalls.java disassembles and names the confirmed bodies.
tools/ghidra/BcallEvidenceStudy.java then provides a read-only listing, reference,
and decompiler dump for a selected set of IDs. For example:
nix develop -c ghidra-analyzeHeadless "$PWD" ti84 \
-process -noanalysis -readOnly -scriptPath tools/ghidra \
-postScript BcallEvidenceStudy.java tools/symbols/bcall_targets.txt \
/tmp/bcall-evidence.txt 4030 4ED6 50C8
Jump-table ID ranges
The dispatcher (bcall_dispatcher) decodes the ID’s top two bits to pick the table page: bit 15 set → page-byte 0x7F (masked & 0x3F → page 0x3F); bit 14 set → 0x7B (→ page 0x3B); with neither bit set it falls through to lookup_bcall_table_page (ram:2ADA). The two tables real bcall IDs use:
0x4xxx–0x7FFF(bit 14 set): the main table on flash page0x3B, entry at offsetID − 0x4000(645 live-confirmed bcalls: 623 IDs also present in the included SDK equates and 22 project-inferred additions).0x8xxx(bit 15 set): the retail boot table is on physical page3F, indexed byID & 0x7FFF. Its real entries occupy IDs0x8018–0x80D2and0x80E4–0x8129; bytes3F:40D5–3F:40E3between those ranges are executable dispatch-stub bytes, not five table entries.D84PBE1.8Xvsupplies the retail page3F;D84PBE2.8Xvsupplies the companion USB boot support page2F. Most entries resolve to3F:addr; USB entries such as_AttemptUSBOSReceive(80E4) and_InitUSB(8108) resolve to2F:addr.tools/ti84re/rom/resolve_bcalls.pyrefuses to emit these targets from a BootFree-substituted page. [confirmed]
Both resolved table formats are 3-byte entries: target address (little endian) plus page byte masked with & 0x3F.
RST shortcuts (fast inlined bcalls) [confirmed]
Five of the RST vectors are 1-byte fast paths for the hottest routines (each JPs to its page-0 handler, which is also reachable as a bcall — the table maps the same address). rst 28h is the bcall dispatcher itself (not a shortcut, and ram:2A2F is not a bcall target); it is listed here only to complete the vector set:
| Opcode | Vector → target | Routine |
|---|---|---|
rst 08h | 0008→1A2F | _OP1ToOP2 (copy FP reg) |
rst 10h | 0010→0E65 | _FindSym (VAT lookup) |
rst 18h | 0018→155C | _PushRealO1 (push OP1 to FPS) |
rst 20h | 0020→1B01 | _Mov9ToOP1 (copy 9 bytes → OP1) |
rst 28h | 0028→2A2F | bcall dispatcher |
rst 30h | 0030→229E | _FPAdd (float add) |
All six match the documented TI-83+/84+ RST assignments — strong cross-confirmation of the table resolution.
bjump — the sibling mechanism (OS-internal cross-page calls)
Besides bcalls, the OS calls its own cross-page routines via bjump. Its
encoding is:
CALL cross_page_jump ; = CALL ram:2B09
.dw addr
.db page
cross_page_jump reads the stacked return address (its caller’s, via an
SP-relative load — it does not POP it), fetches the 2-byte target + 1-byte
page from the inline descriptor there, rewrites the return frame past those 3
bytes, banks the page (& 0x3F), and returns into the target. The target’s
RET then returns to the bjump’s caller, so it behaves like a call that
consumes the 3 inline bytes.
There is a trampoline table in the page-0 address range ram:3B01–ram:3D0A: 87 packed 6-byte entries, each a bjump to a hot OS routine on another page (ram:3D0B already begins a separate CALL ram:2B49 table). The static Ghidra database models it in the page-0/ROM address space; whether the table is copied to RAM at runtime remains a hypothesis. Code invokes a routine by CALL ram:3Bxx into the table. tools/symbols/bjumps.txt lists every entry’s (offset → page:addr); tools/ghidra/RamRoutines.java marks the inline .dw/.db as data and comments each target.
Example: _PutMap’s glyph blitter is reached via the trampoline at ram:3B3D → 07:4588.
Inline bjumps. Besides this trampoline table, the three-line bjump encoding
above appears in packed dispatch tables and inside OS routines. The target returns
to the bjump caller, so cross_page_jump consumes the three inline descriptor
bytes as a non-returning tail-jump. tools/ghidra/FixInlineBjumps.java runs before and
after the scripts that seed parser handlers and reviewed function entries. Each
pass marks every disassembled inline site, including the 87 trampoline-table
entries. Raw byte matches outside disassembled code are not counted. [confirmed]
Limitations
- Keep the BootFree guard in place when regenerating from emulator-derived ROM images.
- Some bcalls are thunks: e.g.
_FindSym’s page-0 entry usescross_page_jumpto reach the real body on page 0x07.