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

Flash memory

TI-84 Plus OS 2.55MP — Flash hardware, boot bcalls, and archive writes.

The TI-84 Plus programs Flash through three distinct layers: ASIC access control, an AMD-compatible command state machine in the Flash chip, and boot-page bcalls that execute their write loops from RAM. This page separates those layers, gives calling conventions and examples for the Flash bcalls, reconstructs their workers byte for byte, and follows a normal Archive prgmA operation into the hardware path.

Evidence layers

The mechanisms below use several evidence sources. A claim marked [confirmed] comes from the local OS 2.55MP image or a complete TilEm execution trace. A claim marked [standard] comes from the named hardware source and agrees with the ROM. Emulator behavior is identified explicitly. It establishes what that emulator implements, not what the physical ASIC or Flash chip does.

LayerMain evidenceWhat it establishes
TI-OS and boot codetools/rom.bin, especially 3D:61AF3D:6BC4 and 3F:47843F:4E56bcall ABI, guards, RAM workers, archive allocation, and status handling [confirmed]
Dynamic executionarchive and GCFLASH TilEm traces plus guarded TilEm, Wabbitemu, and MAME runsROM worker paths, GC sector ordering, execution limits, and native command-state behavior [confirmed] for the pinned emulator runs
ASIC modelTilEm x4_memory.c, x4_io.c, and x4_init.cprotected-byte recognizer, port gates, execution limits, and modeled sector protection [standard]
Flash deviceDatamath’s March 2004 board photograph and Fujitsu MBM29LV800TA data sheetobserved package marking, sector geometry, command cycles, DQ status semantics, and rated limits [standard]
Emulator comparisonpinned TilEm, Wabbitemu, MAME, and jsTIfied sourcemodeled command decode, mutation rules, status reads, timing, and missing ASIC gates [standard]

Write-layer schematic. Bcall entry guards, remaining caller obligations, and RAM-worker execution are [confirmed]. ASIC gate details and the AMD-compatible command state machine are [standard].

Physical organization

Identified board part and compatible family

Datamath’s photographed March 2004 TI-84 Plus board carries a Fujitsu package marked 29LV800TA-70PFTN. Fujitsu’s orderable part number adds its MBM prefix: MBM29LV800TA-70PFTN. This identifies the device on that photographed board. It does not establish one vendor for every TI-84 Plus revision. [standard]

Datamath’s NOR component index also lists AMIC A29L800A, Fujitsu 29LV800, Spansion S29AL008D, and Macronix MX29LV800 as compatible 1 MiB families. Those entries establish a reported compatible family, not which part a particular calculator contains. [standard]

The Fujitsu suffixes and rated limits decode as follows. These are data-sheet limits rather than measurements of a calculator. [standard]

Marking or fieldMeaning
8M (1M × 8/512K × 16)8 Mbit array, used here as one MiB of byte-addressable NOR Flash
TAtop-boot sector geometry
-7070 ns maximum read access
PFTN48-pin TSOP(I), normal-bend package
supply3.0 V-only read, program, and erase
program/erase enduranceminimum 100,000 cycles
byte program8 µs typical, 300 µs maximum
sector erase1 s typical, 10 s maximum

The local ROM image and TilEm’s TI-84 Plus model use 64 logical pages of 16 KiB. A logical Flash page is an ASIC paging unit, not an erase unit. Port 0x06 maps one page into the Z80’s 0x40000x7FFF bank-A window. The Flash device erases the larger physical sector containing the command address. [confirmed] for the ROM page count; [standard] for the device organization.

Data-sheet command and status interface

In byte mode, the Fujitsu device decodes unlock addresses 0xAAA and 0x555. The command table defines the following operations. Address and data cycles after a command prefix are shown separately. [standard]

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

The data sheet defines no CFI query command for this part. A reset returns the device to array-read mode, including after DQ5 reports an exceeded timing limit. [standard]

The status outputs distinguish more states than the boot workers consume: [standard]

BitFujitsu data-sheet behavior
DQ7complements programmed data bit 7 while program is active; reads 0 during erase and the array value after completion
DQ6toggles during program, erase, and the sector-erase timeout window
DQ5indicates exceeded program/erase timing; it can also follow an attempt to program a nonblank location without erasing
DQ3distinguishes the open sector-erase command window from the active erase algorithm
DQ2toggles for an erasing or erase-suspended sector and helps distinguish erase states from program states

Erase suspend applies only to sector erase, including its 50 µs timeout window. The device ignores it during chip erase and byte program. The data sheet bounds suspend latency at 20 µs. DQ7 becomes one and DQ6 stops toggling; DQ2 continues toggling when the suspended sector is read. Reads and programs remain available in sectors that are not being erased. [standard]

Fujitsu autoselect returns manufacturer 0x04 and top-boot byte-mode device 0xDA at byte-mode offsets XX00 and XX02. Offset XX04 reports the selected sector’s protection state in DQ0. Wabbitemu and MAME instead return manufacturer 0x01 with the same device code. Their values identify an AMD-compatible emulator model, not the Fujitsu package in the Datamath photograph. [standard]

Retail ROM command coverage

The retail ROM’s instruction-aligned direct stores to logical unlock addresses 0x6AAA and 0x5555 occur at 11 locations. They belong to three length-prefixed command bodies. [confirmed]

Command bodyDirect unlock-address storesCommand use
Page-3D program worker at 3D:730A3D:7342, 3D:734B, 3D:7354AA 55 A0, then program data through LDI
Boot erase worker3F:4C48, 3F:4C51, 3F:4C5A, 3F:4C63, 3F:4C6CAA 55 80 AA 55 30
Boot program worker3F:4CFB, 3F:4D04, 3F:4D0DAA 55 A0, then program data through LDI

No direct unlock-address candidate has a nearby command-valued LD A,n for chip erase (0x10), fast-mode entry (0x20), autoselect (0x90), erase suspend (0xB0), or CFI query (0x98). The worker bodies use byte program, sector erase, and array reset. [confirmed]

tools/flash_rom_commands.py performs the structural match, and tools/analyze_flash_rom_commands.py emits text or JSON. The scan deliberately does not treat raw literals as commands. Linear disassembly can decode data as instructions, indirect stores can hide a destination, and a standalone command can target an address other than the two unlock addresses. The result therefore establishes coverage of exact LD (nn),A candidates, not universal absence of every dynamically constructed command. [confirmed]

Sector geometry

The Fujitsu MBM29LV800TA data sheet defines the top-boot geometry below. TilEm, Wabbitemu, MAME, and jsTIfied use the same boundaries. [standard]

Physical rangeSizeLogical pages or page portion
0x0000000x0EFFFF15 × 64 KiBpages 003B, four pages per sector
0x0F00000x0F7FFF32 KiBpages 3C3D
0x0F80000x0F9FFF8 KiB3E:40003E:5FFF
0x0FA0000x0FBFFF8 KiB3E:60003E:7FFF
0x0FC0000x0FFFFF16 KiBpage 3F

The two halves of logical page 3E are separate 8 KiB sectors. This is why _EraseCertificateSector accepts logical address 0x4000 or 0x6000. Page 3F is one 16 KiB boot sector. A sector erase directed anywhere in an ordinary archive page erases all four 16 KiB pages in its 64 KiB sector. [confirmed] for the certificate API; [standard] for chip geometry.

Three independent protection mechanisms

“Flash protection” can refer to three different controls. Treating them as one switch obscures several ROM checks.

Flash command lock — port 0x14

Port 0x14 controls whether writes reach the Flash command state machine. Writing 1 unlocks Flash command writes; writing 0 locks them. The write is accepted only after the ASIC observes this byte sequence fetched from a privileged Flash region: [standard]

00 00 ED 56 F3 D3

The usual instruction spelling is:

nop
nop
im 1
di
out (0x14),a

The ASIC recognizes fetched bytes rather than the semantic instruction stream. WikiTI documents alternate instruction sequences that produce the same bytes. TilEm’s TI-84 Plus model advances its recognizer only when the bytes come from physical 0xB00000xBFFFF or 0xF00000xFFFFF; other Flash or RAM reads reset the recognizer. It accepts the following port-0x14 output only in recognizer state 7. [standard]

Unlocking port 0x14 does not program a byte. It allows subsequent memory writes to reach the Flash chip, where they must still form a valid AMD command sequence. [standard]

The public write and erase bcalls expect Flash to be unlocked by their caller. The archive record writer at 3D:64AA performs the protected port-0x14 sequence itself before calling those APIs. [confirmed]

Physical sector protection

TilEm assigns protection group 1 to physical 0xB00000xBFFFF and 0xFC0000xFFFFF. Port 0x21 bits 0–1 select the modeled override group while Flash is unlocked. A command can therefore pass the port-0x14 lock and still be rejected for a protected physical sector. [standard]

The retail boot programs port 0x21 = 0 at 3F:41DC. Its low field also selects model-specific Flash page bounds, while bits 4–5 configure the RAM execution mask. See ASIC status, identity, protection, and GPIO for the ROM uses, emulator equations, and public size tables. [confirmed] for the boot write; [standard] for the modeled protection behavior.

This protection is separate from the safe bcall checks. For example, _WriteAByte permits starting page 3E at the software layer, while the hardware still controls whether the affected sector is writable. [confirmed] for the bcall; [standard] for the ASIC model.

Read and execution protection

The certificate page is read-censored while Flash is locked. WikiTI documents the model-selected page as 1E, 3E, or 7E; TilEm returns 0xFF for locked reads of page 3E on its TI-84 Plus model. [standard]

Ports 0x22 and 0x23 define a forbidden Flash-execution interval. TilEm includes both endpoints, while Wabbitemu allows the lower page. The retail boot writes 0x08 and 0x29. Ports 0x25 and 0x26 bound executable RAM in 1 KiB units. Both emulators accept writes to these protected ports only while Flash is unlocked. See Execution protection for the ROM sequence, exact equations, guarded Flash and RAM execution runs, and unresolved physical boundaries. [confirmed] for the boot values and pinned emulator runs; [standard] for the source models.

These execution limits explain why the byte-poke loops run at ramCode (0x8100). They are distinct from the Flash chip’s inability to provide ordinary array data while a program or erase operation is active. [confirmed] for the RAM workers; [standard] for the execution controls.

Boot-page Flash API

The retail boot bcall table maps the Flash APIs below. The bcall ID is the word after rst 28h; the body address is where the resolved code executes. [confirmed]

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

WikiTI’s ABI agrees with these register uses and says the block-write source must be RAM. The ROM adds exact page guards, call-site checks, return values, and boundary behavior described below. [standard] for the published ABI; [confirmed] for the additions.

Programmer-facing bcall guide

These bcalls expose the command workers. They do not provide the allocation, battery policy, ownership checks, transaction journal, or gate management used by the archive subsystem. A normal program that wants to archive or unarchive a variable should use _Arc_Unarc rather than choose a raw Flash address. Low-level calls are appropriate only when the caller owns the target region and also owns the surrounding recovery policy. [confirmed] for the bcall behavior; [standard] for using the public variable API.

Shared preconditions and hygiene

Every modifying bcall below has these caller obligations: [confirmed] for the ROM behavior unless marked otherwise.

  • Open the protected port-0x14 gate before the call and close it on every exit. The Flash bcalls do neither operation. Code executing from ordinary RAM cannot satisfy the privileged-fetch sequence by copying its bytes into RAM.
  • Check battery state before opening the gate. The OS archive path calls _Chk_Batt_Low before its own Flash transaction, but the boot workers do not.
  • Establish ownership of the complete physical sector. Flash programming can only clear bits from one to zero. Restoring a zero bit to one requires an erase, which affects 64 KiB for ordinary pages and the smaller top-boot sectors shown under Sector geometry. [standard]
  • Keep a recovery record outside the sector being changed if interruption must be survivable. The raw bcalls have no power-loss journal.
  • Use rst 28h with the bcall ID. Do not call the page-3F body address. The raw write and erase cores reject a direct caller whose immediate return address is at or above 0x8000.
  • Keep the stack, source, and destination buffers away from 0x81000x817B. The launcher overwrites that range with the block-program worker. The erase worker occupies 0x81000x8151. The launcher also writes its saved IFF state at 0x82A2.
  • Keep IY at the OS flags base for the write calls. The accepted block path clears (IY+0x25).1; its unused low-source branch can set the same unnamed scratch bit. _WriteAByte additionally overwrites the first byte of OP1 at 0x8478.
  • Treat A, BC, DE, HL, flags, OP1, and the scratch locations above as clobbered when their selected path uses them. The launchers preserve IX and restore the interrupt-enabled state that existed on entry.
  • Validate the arguments before interpreting the result. Several rejected or no-op paths return flags that resemble success. After a validated nonempty call, require A=0, then read back the complete programmed span or erased sector. A locked write can return A=0, Z in TilEm without changing Flash.

The labels “safe” and “unsafe” describe only the page-3E software guard. Neither safe entry checks the port-0x14 gate, physical protection, archive ownership, destination address, length, battery, or power-loss state. [confirmed]

Choosing a write bcall

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

The block worker expects DE in 0x40000x7FFF and a RAM source with HL >= 0x8000. The ROM does not enforce either condition. For nonzero length $n$, validate the final target before the call:

$$ p_{final} = p + \left\lfloor \frac{(DE - 0x4000) + n - 1}{0x4000} \right\rfloor $$

Also require DE in the banked window, ensure the RAM source plus $n$ does not wrap, and keep the source outside the worker and scratch ranges. For _WriteFlash, require every page through $p_{final}$ to stay below 3E. Crossing from page 3D toward 3E does not stop cleanly: the worker wraps DE to 0x4000 but leaves page 3D mapped. [confirmed]

_WriteFlash

_WriteFlash = 80C9h is the ordinary block entry. Inputs are A=page, DE=destination, BC=length, and HL=RAM source. It masks the page with 0x3F, rejects page 3E, and then enters _WriteFlashUnsafe, which rejects page 3F. A validated, nonempty successful call returns A=0, Z. A worker failure returns A=0x3F, NZ. [confirmed]

On success, BC=0, while HL and DE point one byte beyond the source and destination spans. On a program failure, HL and DE identify the failing bytes and BC retains the decrement already performed by LDI. The page guards are exceptional: they return nonzero A with Z. A zero-length accepted call returns the masked page and NZ without launching the worker. [confirmed]

Use this entry only after validating the entire span. Its initial page check does not protect a call that begins below page 3E and later crosses a page or sector boundary.

The executable example programs two bytes from RAM at 08:4100:

    ld a,$08
    ld de,$4100
    ld hl,writeflash_payload
    ld bc,writeflash_payload_end-writeflash_payload
    rst $28
    .dw $80C9
    or a
    jp nz,flash_failed

The guarded runner seeds both target bytes to 0xFF, requires AF=0x0044, and verifies A5 5A in both the Flash array and a _FlashToRam buffer. [confirmed] for pinned Wabbitemu execution.

_WriteFlashUnsafe

_WriteFlashUnsafe = 8087h has the same block ABI and worker results as _WriteFlash. It omits only the page-3E rejection. The core still masks A to six bits, rejects page 3F, checks the call frame, and accepts a zero length as a no-op. [confirmed]

The guarded retail-ROM usage probe calls this entry with A=0x3E, programs 3C C3 at 3E:4100, and reads the same pair back through _FlashToRam. The bcall returns AF=0x0044. [confirmed] for pinned Wabbitemu execution.

    ld a,$3E
    ld de,$4100
    ld hl,writeflashunsafe_payload
    ld bc,writeflashunsafe_payload_end-writeflashunsafe_payload
    rst $28
    .dw $8087
    or a
    jp nz,flash_failed

“Unsafe” does not mean that the routine bypasses physical protection. Port 0x14, the port-0x21 sector group, and the Flash chip still decide whether the command reaches and changes the array. Its page-3E access makes this entry suitable for OS-owned certificate work, not for ordinary archive data. [confirmed] for the software entry; [standard] for the hardware gates.

_WriteAByteSafe

_WriteAByteSafe = 80C6h takes A=page, DE=destination, and B=byte. It masks the page, rejects page 3E, and falls into _WriteAByte. The shared unsafe core later rejects page 3F. Its accepted path therefore has the same page exclusions as _WriteFlash. [confirmed]

An early page-3E rejection leaves BC, DE, HL, and OP1 untouched. Page 3F reaches the byte wrapper first, so that rejection has already stored B at OP1, set BC=1, and set HL=0x8478. This difference matters to a caller that tries to infer whether scratch state changed from the flags. [confirmed]

The guarded retail-ROM usage probe exercises the accepted path at 08:4102. It programs 0xFE → 0xFC, returns AF=0x0044, and obtains 0xFC through _FlashToRam. [confirmed] for pinned Wabbitemu execution.

    ld a,$08
    ld de,$4102
    ld b,$FC
    rst $28
    .dw $80C6
    or a
    jp nz,flash_failed

_WriteAByte

_WriteAByte = 8021h takes A=page, DE=destination, and B=byte. It stores B in OP1, sets HL=0x8478 and BC=1, then enters _WriteFlashUnsafe. It permits page 3E and rejects page 3F. A successful call returns A=0, Z, BC=0, HL=0x8479, and DE one byte beyond the target. OP1 retains the programmed byte. [confirmed]

The guarded retail-ROM usage probe calls this entry on page 3E, programs 0xFE → 0xF8 at 3E:4102, leaves OP1=0xF8, and returns AF=0x0044. _FlashToRam returns 0xF8. [confirmed] for pinned Wabbitemu execution.

    ld a,$3E
    ld de,$4102
    ld b,$F8
    rst $28
    .dw $8021
    or a
    jp nz,flash_failed

Use the byte entries for monotonic state changes such as 0xFE → 0xFC or 0xFC → 0xF0. A request that needs any 0→1 transition requires sector erase and reconstruction. A requested byte with the same DQ7 as the stored byte can produce false success when the ASIC gate blocks the command, so verify the byte after every call. [confirmed] for the ROM and pinned TilEm result; [standard] for NOR programming direction.

_EraseFlashPage

_EraseFlashPage = 8084h takes A=page. It masks the page to six bits, chooses address 0x4000, rejects page 3E, and enters _EraseFlash. For page zero it changes the address to 0x0000. It does not reject page 3F. [confirmed]

The name refers to the logical page used to select a sector. It does not limit the erase to 16 KiB. On an ordinary archive page, the command erases the containing 64 KiB sector. Initialize DE to a writable Flash address in the same mapping before the call; the DQ5 failure tail writes reset byte 0xF0 through undocumented DE. [confirmed] for the worker; [standard] for erase geometry.

A successful erase returns A=0, Z. A worker failure returns A=0xF1, NZ. The page-3E rejection instead returns A=0x3E, Z. Prevalidate the page, check A=0, and verify the complete physical sector. [confirmed]

The guarded retail-ROM usage probe erases through page 0C, returns AF=0x0044, and reads 0xFF back at 0C:4000. [confirmed] for pinned Wabbitemu execution.

    ld a,$0C
    ld de,$4000
    rst $28
    .dw $8084
    or a
    jp nz,flash_failed

_EraseFlash

_EraseFlash = 8024h takes A=page and HL=an address in the selected sector. It performs no page mask or page guard. The worker maps A, issues a sector-erase command through HL, and returns the same success or failure values as _EraseFlashPage. BC, DE, and HL are otherwise retained by the erase path, but failure can write 0xF0 to the address in DE. [confirmed]

Choose this entry when the target must be an address other than the page start, including a top-boot sector boundary. Set DE=HL defensively so the failure reset targets Flash rather than arbitrary RAM. This convention avoids the worker’s underspecified failure write; the public ABI itself does not require or synthesize it. [confirmed] for the write; [standard] for the Flash reset command.

The guarded retail-ROM usage probe passes HL=DE=0x4567 on page 10. The bcall returns AF=0x0044, and _FlashToRam reads 0xFF from the same interior sector address. [confirmed] for pinned Wabbitemu execution.

    ld a,$10
    ld hl,$4567
    ld de,$4567
    rst $28
    .dw $8024
    or a
    jp nz,flash_failed

_EraseCertificateSector

_EraseCertificateSector = 8060h accepts any HL whose high byte is 0x40 or 0x60. It does not require L=0. It loads page 3E and calls _EraseFlash, selecting one of the two 8 KiB certificate sectors. Other high bytes return without work. [confirmed]

The wrapper restores the caller’s AF after both accepted and rejected calls. It therefore hides worker success and failure as well as its own input rejection. Preserve the certificate through its OS-owned rebuild protocol and verify the selected sector; flags are not a result channel for this bcall. [confirmed]

The guarded retail-ROM usage probe seeds AF=0xA545 and passes HL=DE=0x6001. The returned AF remains 0xA545, while _FlashToRam reads 0xFF from 3E:6001. This dynamically exercises the accepted nonzero-L path and the second 8 KiB certificate sector. [confirmed] for pinned Wabbitemu execution.

    ld hl,$A545
    push hl
    pop af
    ld hl,$6001
    ld de,$6001
    rst $28
    .dw $8060

Return and side-effect matrix

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

This matrix explains why jr z,success is insufficient. Validate page, address, span, and nonzero length first. Then test A=0 and verify the array.

Checking results

The executable examples above assume that trusted OS or boot code already opened port 0x14, checked the battery, established sector ownership and recovery state, and will relock Flash on every exit. Copying an unlock byte sequence into RAM does not satisfy the ASIC’s privileged-fetch rule.

After a validated nonempty program or erase call, require A=0. Save the target page and address before the call because the write worker advances registers. Read the programmed span through _FlashToRam and compare every byte with the source. For erase, inspect the complete physical sector, not only the selected address. Every failure and success path must reach the trusted owner’s relock and recovery epilogue. [confirmed] for the return and clobber rules; [standard] for physical erase scope.

Reading back with _FlashToRam

_FlashToRam = 5017h, body 3D:6745, copies BC bytes from Flash at A:HL to RAM at DE. It advances the mapped Flash page when HL crosses 0x8000 and restores the previous port-0x06 mapping after its RAM worker returns. It does not need the Flash write gate for ordinary readable pages. [confirmed]

The call consumes BC and advances HL and DE. It uses worker RAM beginning at 0x8100 and page scratch at 0x9868, so a verification buffer must avoid those locations while the copier runs. Locked certificate-page reads remain subject to the ASIC’s separate read protection. [confirmed] for the ROM scratch and worker; [standard] for the read gate.

The executable example reads back the complete two-byte _WriteFlash vector:

    ld a,$08
    ld hl,$4100
    ld de,writeflash_copy
    ld bc,writeflash_payload_end-writeflash_payload
    rst $28
    .dw $5017

Executable example validation

tools/emulator-probes/flash-bcall-usage.asm is the guarded executable form of the examples above. It invokes _WriteFlash, _WriteFlashUnsafe, _WriteAByteSafe, _WriteAByte, _EraseFlashPage, _EraseFlash, _EraseCertificateSector, and _SetFlashLowerBound. It reads every changed location through _FlashToRam. The six calls with result-bearing A values branch to a failure loop unless A=0; the probe also stores every return so the runner can check the complete result. [confirmed]

The nine short bcall call sequences on this page carry an executable-snippet tag. The reusable tools/executable_snippets.py parser requires their text to match the same tagged regions in the assembled probe byte for byte. The tools/check_executable_snippets.py CLI exposes that check. This catches documentation drift; Wabbitemu execution supplies the runtime result below.

On 2026-08-10, the hash-guarded Wabbitemu adapter booted the exact OS 2.55MP ROM, established the retail protection state, injected the 264-byte program into RAM, and opened only Wabbitemu’s in-memory Flash gate. The run reached every named public entry. The shared _WriteFlashUnsafe core ran four times, the _WriteAByte body twice, and the _EraseFlash core three times because their safe and specialized wrappers fall through or call into them. Seven _FlashToRam calls brought the total to 14 RAM-worker entries. No execution violation occurred. [confirmed] for this pinned emulator run.

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

The assembly source SHA-256 was ba91fa8a4d1d7c816b742a426dbb0216f927ec209f368534a13748d4683b42e7; the assembled machine-code SHA-256 was 8f9ca5975c418871ba831c3536cba6e7e4f9f368520e1ad37650ef9c54d9249c. See “Retail Flash bcall usage probe” in tools/dynamic-tracing.md for the guarded reproduction command. This execution validates the snippets against the original ROM bodies under pinned Wabbitemu. It does not validate the privileged port-0x14 sequence, allocation or journaling, interruption, timing, or behavior of a physical Flash device.

_WriteFlash entry paths

The four write entry points converge on the core at 3F:4CA6. [confirmed]

flowchart TD
    WF["_WriteFlash · 3F:4C8F<br/>mask page; reject 3E"] --> U["_WriteFlashUnsafe · 3F:4CA6"]
    WS["_WriteAByteSafe · 3F:4C9A<br/>mask page; reject 3E"] --> W1["_WriteAByte · 3F:4C9F<br/>copy B to OP1; BC=1"]
    W1 --> U
    U --> G["direct-call and page-3F guards"]
    G --> R["copy worker to 0x8100 and execute"]

Safe and unsafe page guards

_WriteFlash masks A with 0x3F and returns immediately for page 3E. _WriteAByteSafe does the same before falling into _WriteAByte. The unsafe core masks the page again and returns for page 3F. Safe writes therefore reject both pages 3E and 3F. [confirmed]

_WriteAByte enters the unsafe core without the page-3E test. It stores B in OP1 at 0x8478, replaces HL with that address, and sets BC=1. It permits page 3E but still inherits the page-3F rejection. [confirmed]

The page guards return the result of an equality comparison. Rejected page 3E and page 3F calls therefore return Z, the same condition as a successful worker. Callers must obey the page contract; Z alone does not prove that a write occurred. [confirmed]

Direct-call-site check

Both _WriteFlashUnsafe and _EraseFlash inspect the immediate stacked return address:

ex (sp),hl
bit 7,h
ex (sp),hl
ret nz

The routine returns NZ when that address is at least 0x8000. It does so before masking A. A normal bcall passes because the bcall dispatcher interposes a low-memory return frame; the archive trace reaches 3F:4CA6 with the relevant return address at 0x2B41. This is a direct-call-site check. It does not prevent a RAM program from invoking the public bcall through rst 28h. [confirmed]

Zero-length write

After the guards, _WriteFlashUnsafe saves AF, tests B|C, and restores AF when the length is zero. A zero-length call therefore returns the masked page in A and the flags from the preceding CP 0x3F. An accepted page is not equal to 0x3F, so this no-op returns NZ. It never copies or executes the RAM worker. [confirmed]

Early-return trace

The read-only entry-returns fixture runs on the unmodified ROM. It verifies the first eight bytes at 3F:4CA6, never writes port 0x14, and exercises four paths that return before worker launch. analyze_flash_trace.py reports zero CPU write attempts targeting mapped Flash. The captured bcall-visible values are: [confirmed] for TilEm execution of the ROM paths.

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

The direct call reaches 3F:4CA6 and returns from 3F:4CAA; it does not reach the page mask at 3F:4CAB. The zero-length call reaches 3F:4CB3, branches to 3F:4CC6, restores the saved comparison result, and returns. [confirmed]

Byte-entry return trace

_WriteAByteSafe checks page 3E before entering _WriteAByte. A page-3E rejection returns from 3F:4C9E without changing OP1, BC, DE, or HL. Page 3F passes that first comparison. _WriteAByte then stores B at OP1 (0x8478), loads HL=0x8478 and BC=1, and reaches the unsafe core. The page-3F rejection at 3F:4CAF therefore exposes those wrapper side effects even though no worker runs. [confirmed]

A direct CALL 3F:4C9F from RAM also performs the byte-wrapper setup before the unsafe core inspects the return address at 3F:4CA6. It returns from 3F:4CAA with OP1, BC, and HL changed. [confirmed]

The read-only byte-entry-returns fixture verifies all 16 bytes from 3F:4C9A through 3F:4CA9 on the unmodified ROM. It restores the original OP1 byte before returning and never unlocks Flash. Its machine-code SHA-256 is 6851da991e031ea7df1a31ab3bf62816ad992e3d1946566d31b0a02e16dd50e1. The trace contains zero CPU write attempts targeting mapped Flash. [confirmed] for the fixture and TilEm execution.

ClockCall and triggerReturn AFBCDEHLOP1
187,804,393_WriteAByteSafe, page 0x7E3E0x3E420x22330x44550x66770x11 unchanged
187,806,001_WriteAByteSafe, page 0x7F3F0x3F420x00010x66770x84780x44 from B
187,807,587_WriteAByte, page 0x7F3F0x3F420x00010x77880x84780x55 from B
187,807,892direct CALL 3F:4C9F, A=0xA50xA5910x00010x88990x84780x66 from B

The two page guards still return Z. That condition describes the final comparison, not whether _WriteAByte changed its scratch registers or launched a worker. [confirmed]

RAM-worker launcher

boot_ram_worker_launcher at 3F:48C5 launches length-prefixed boot workers. IX points at this packed descriptor: [confirmed]

typedef struct {
    uint16_t length;
    uint8_t code[];
} RamWorkerDescriptor;

The flexible code[] member describes the serialized ROM object. Ghidra applies the reusable type to the two-byte header only because each payload has a different length; the payload begins at descriptor + 2. The launcher copies descriptor->length bytes from there to ramCode at 0x8100. It then restores the caller’s HL, DE, and BC and calls the copied code. [confirmed]

The interrupt wrapper at 3F:48EE records IFF2 from LD A,I in 0x82A2, disables interrupts, and returns to the launcher. After the worker returns, 3F:48E1 executes EI only if interrupts were enabled before entry. The worker therefore runs atomically while preserving the caller’s prior interrupt-enabled state. [confirmed]

WorkerPrefixSource bytesRAM destination
sector eraseboot_flash_erase_worker_descriptor at 3F:4C3B, 0x0052descriptor + 2, at 3F:4C3D3F:4C8EramCoderamCode + 0x51
block programflash_program_worker_descriptor at 3F:4CC8, 0x007Cflash_program_worker_code at 3F:4CCA3F:4D45ramCoderamCode + 0x7B

Page 3D contains a relocated copy of the launcher at 3D:678C. It runs flash_to_ram_worker_descriptor at 3D:6761 and certificate_worker_descriptor at 3D:7308. Its interrupt wrapper at 3D:67B5 has the same IFF2-save, DI, conditional-EI behavior as the boot launcher. The inferred name ram_worker_launcher therefore describes both call paths. [confirmed]

Block-program worker

The block worker repeats a four-write AMD byte-program sequence for each source byte. It temporarily maps fixed pages 02 and 01 so the command addresses appear in bank A, then restores the target page for the data write. [confirmed]

StepMapped pageLogical writeValue
1020x6AAA0xAA
2010x55550x55
3020x6AAA0xA0
4targetDEbyte from (HL)

The device decodes the physical low 12 address bits. Page 02, logical 0x6AAA is physical address 0xAAAA; page 01, logical 0x5555 is physical 0x5555. Their low 12 bits are the Fujitsu byte-mode unlock addresses 0xAAA and 0x555. [confirmed] for the ROM addresses; [standard] for device decoding.

Completion polling

After LDI writes a byte and advances HL, DE, and BC, the worker steps back to compare the programmed byte with the target read: [confirmed]

  1. XOR source and target, then test bit 7. Equal DQ7 means the byte completed.
  2. If DQ7 differs, restore that same target byte and test its DQ5 bit.
  3. Clear DQ5 repeats the first target read.
  4. Set DQ5 causes one final target read and DQ7 comparison.
  5. A second DQ7 mismatch takes the failure path.

This is the algorithm in Fujitsu figure 22. During programming, DQ7 returns the complement of the requested data bit until completion. DQ5 indicates an exceeded timing limit. The data sheet requires the second DQ7 check because DQ7 and DQ5 may change simultaneously. [standard]

Return state

On success, the worker writes reset command 0xF0 at the last target address, forces port 0x06 to page 3F, and returns A=0, Z. On failure, 3F:4D3D3F:4D45 writes 0xF0 at the failing target, loads A=0x3F for the page-select output, executes OR A, and returns A=0x3F, NZ. [confirmed]

After full success, HL and DE point one byte beyond the completed span, and BC=0. On failure, the branch occurs before 3F:4D2C and 3F:4D2D restore the backed-up pointers. HL and DE therefore identify the failing source and target bytes, while BC retains the decrement performed by LDI. _WriteAByte destroys all three public ABI registers. [confirmed]

Forcing page 3F is part of the worker ABI. The outer bcall dispatcher restores the page mapping required by its caller after the boot routine returns. A direct caller that passes the low-address check must account for this mapping change itself. [confirmed]

Internal certificate-page programmer

certificate_write_byte at 3D:72E5 launches a second byte-program worker. It sets BC=1, clears (IY+0x25).1, normalizes the target page for the current calculator model, and passes certificate_worker_descriptor to ram_worker_launcher. Its code begins at descriptor + 2 and contains 129 bytes at 3D:730A3D:738A. flash_program_worker_code contains 124 bytes at 3F:4CCA3F:4D45. [confirmed]

The only direct call to certificate_write_byte is 3D:4332, inside certificate_copy_from_flash at 3D:431A. The loop obtains an ordinary Flash page from 3D:5258, stages one byte in OP1 with _FlashToRam, selects the model-specific certificate page through model_certificate_page at 3D:738B, and programs the byte at the current certificate destination. Direct callers at 3D:426A and 3D:4715 reach this loop. [confirmed]

certificate_copy_to_flash at 3D:434B performs the reverse transfer. Its prologue at 3D:433F obtains and erases the ordinary Flash destination page. The loop stages a certificate byte in OP1 through 3D:42AC, obtains the ordinary destination page through 3D:5258, and calls _WriteFlashUnsafe = 8087h. The direct callers at 3D:4127 and 3D:4707 pass destination address 0x4000. [confirmed]

Both loops belong to certificate_rebuild_dispatch at 3D:40F1. The dispatcher stores its mode byte at 0x9C20, copies certificate data to an ordinary Flash work area at 3D:4127, rebuilds mode-dependent certificate fields, erases a model-selected certificate half through 3D:4252, and can copy the work area back at 3D:426A. This identifies the data directions and the rebuild role. Direct calls and page-0 bjump calls identify an owner for each mode. [confirmed]

The dispatcher operates on the last 0x216 bytes of either 8 KiB certificate half. Its fixed offsets and lengths divide that tail into four contiguous blocks: [confirmed]

Half-relative offsetLengthRange
0x1DEA0x660x1DEA0x1E4F
0x1E500xC80x1E500x1F17
0x1F180xC80x1F180x1FDF
0x1FE00x200x1FE00x1FFF

The adjacent App-restriction bytes make the complete decoded tail easier to address as a partial structure based at half offset 0x1DD2: [confirmed]

typedef struct {
    uint8_t restriction_control;        // +0x000, half offset 0x1DD2
    uint8_t restriction_record[13];     // +0x001, half offset 0x1DD3
    uint8_t unresolved_1de0_1de9[10];   // +0x00E
    uint8_t gc_recovery[0x66];          // +0x018, half offset 0x1DEA
    uint8_t ti84_app_trials[0xC8];      // +0x07E, half offset 0x1E50
    uint8_t alternate_model_span[0xC8]; // +0x146, half offset 0x1F18
    uint8_t validity[0x20];             // +0x20E, half offset 0x1FE0
} CertificateMetadataTail;

The unresolved_1de0_1de9 name deliberately records only its bounds. The ROM evidence does not identify an owner for those ten bytes. The notation below uses certificate_tail for a CertificateMetadataTail view of the selected certificate half. BuildTypes.java registers this reusable type but does not apply it at one fixed address: the ROM selects the certificate half at runtime, so certificate_tail means a conceptual pointer to selected_half + 0x1DD2, not a global Ghidra symbol. [confirmed]

Six helpers at 3D:52273D:5256 add fixed or model-selected offsets to _GetCertificateStart’s result. Raw CALL scanning finds the complete direct caller sets without relying on disassembler labels: [confirmed]

EntrySelected offsetDirect callers
3D:52270x1DD33D:42D4, 3D:7D7A
3D:522D0x1FE03D:42B3, 3D:4589, 3D:4654, 3D:47A8, 3D:521D, 3D:5448
3D:52330x1F183D:414B, 3D:4288, 3D:42EA, 3D:42F2, 3D:42FD, 3D:4306, 3D:47B1, 3D:493D, 3D:4CBD, 3D:4F14, 3D:5080, 3D:5184, 3D:51A8, 3D:538F
3D:52410x1DEA3D:4274, 3D:4298, 3D:42A3
3D:5247model-selected3D:490F, 3D:5385, 3D:548F, 3D:5C0E
3D:52520x1FE03D:430E

The model-selected helper calls 00:1837. That probe reads port 0x02, masks bit 7, and preserves the resulting flags while restoring A and BC. 3D:5247 branches to the fixed 0x1F18 helper when the bit is clear and falls through to 0x1E50 when it is set. The resolved TI-84 Plus traces read 0xE1, 0xE3, or 0xE7, so every observed TI-84 Plus state selects 0x1E50. [confirmed]

Wabbitemu independently returns a base value with bit 7 set for models at or above its TI_84P enum and clear for its TI-83 Plus family. This supports the family split implemented by the ROM but remains evidence about the emulator, not a physical measurement. [standard]

Consequently certificate_tail.ti84_app_trials, at 0x1E500x1F17, is the active App-trial table on TI-84 Plus. When port-0x02 bit 7 is clear, the same clear, write, query, and display paths select certificate_tail.alternate_model_span at 0x1F180x1FDF. TI-84 Plus rebuild modes 0 and 2 still stage or replace that alternate-model span together with validity metadata, but no TI-84 Plus per-entry semantic accessor to that span has been identified. Giving it another TI-84 Plus field name would exceed the evidence. [confirmed] for selection and access; [hypothesis] for any further TI-84 Plus meaning.

The helper calls in each dispatch branch identify which span receives mode-specific replacement data. Other helpers clone retained spans from the active half to the opposite half. Mode 4 also exports 0x1E500x1F17 to 0x8000 and 0x1DD30x1DDF to 0x80F0. [confirmed]

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

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

ModeDirect callByte-pinned gate context
03D:66C7The full-reset path at 35:7205 reaches 3D:6673 through the page-0 trampoline at 00:2DC3. 3D:6673 opens the gate at 3D:6680; the tail at 3D:66CA jumps to the shared relock routine.
13D:5774The App-deletion path at 3D:4018 and invalid-App cleanup at 3D:5F71 call 3D:5759. The first path opens at 3D:400A; the second opens at 3D:5F28.
23D:437EThe certificate receive path reaches 3D:4721; Flash App receive preparation reaches 3D:5094. Both inherit gate state.
53D:51D7The enclosing path opens at 3D:70DA; later exits relock at 3D:7194, 3D:71AA, or 3D:71E4.
63D:7D87_RemoveAppRestrictions at 3D:7C1B opens at 3D:7C46, calls the rebuild wrapper at 3D:7D82, and relocks at 3D:7C8C.

Modes 3 and 4 enter through the page-0 bjump stub at 00:2B77. The stub’s inline descriptor is F1 40 7D, which resolves to 3D:40F1. Both callers belong to gc_recovery_preflight at 3C:7219, which opens the Flash gate at 3C:7228: [confirmed]

ModePage-3C callCall chainRole
33C:75583C:7219 → 3C:724A → 3C:7544 → 3C:7558 → 00:2B77 → 3D:40F1Rewrite the 0x1DEA0x1E4F recovery metadata after an archive-sector operation in the recovery loop.
43C:73133C:7219 → 3C:72A5 → 3C:7313 → 00:2B77 → 3D:40F1Initialize the certificate-backed recovery metadata before the loop.

Mode 0 initializes the OS/App-validity tail during full reset. 3D:6673 erases ordinary Flash page 8, fills the 0xE8-byte replacement buffer with 0xFF, and stores 0xFE at 0x836D. That RAM byte corresponds to certificate offset 0x1FE0. On models other than the TI-83 Plus, the routine also stores 0x7F in the next byte before invoking mode 0. [confirmed]

Mode 1 clears a two-byte per-App trial entry when an App is removed. 3D:5759 stages the model-selected table, converts the App page to a two-byte index, writes FF FF, and invokes mode 1. On TI-84 Plus that table is 0x1E500x1F17. The table’s use as an App trial table is also ROM-confirmed. The App receive path writes the same two-byte entry at 3D:5BB7. The App-information path at 36:70B5 calls the reader at 3D:5466, displays the ROM string "Trials Remaining:" at 01:41AA, and prints values derived from the two bytes. The direct mode-1 callers at 3D:4018 and 3D:5F71 belong to App deletion and invalid-App cleanup. [confirmed]

One mode-2 owner is the certificate receive path. The link header dispatcher selects certificate type 0x25 at 3C:565D. After _FindFirstCertField, a field selector with H=3 and L & 0xF0 = 0x10 reaches the page-0 bjump stub at 00:2BFB from 3C:5714. That stub targets 3D:4771. Its certificate-half rotation path calls 3D:46EE, which invokes mode 2 at 3D:4721. This pins mode 2 to rebuilding 0x1F180x1FFF for that certificate-field selector. The other mode-2 caller belongs to Flash App receive preparation. Header type 0x24 enters at 3C:550D. The per-page call at 3C:55BD reaches 3D:73BE through the page-0 stub at 00:2D81. 3D:73BE checks the App page, clears its App-validity bit when necessary, and reaches 3D:5019 through 3D:5356. That path stages 0x1F180x1FFF and invokes mode 2 at 3D:5094. [confirmed]

These owners establish where mode 2 is used. They do not establish any additional TI-84 Plus field meaning for the alternate-model App-trial span at 0x1F180x1FDF. [hypothesis] for such an additional meaning.

The mode-4 path fills the model-selected journal buffer at 0x82A5 or 0x8000, initializes its phase bytes at 3C:72D13C:730D, and invokes the dispatcher. The recovery loop reaches mode 3 through 3C:7544. That routine selects an archive sector, calls _EraseFlashPage = 8084h, updates the RAM journal fields at 3C:7568 and 3C:7576, then persists the 0x66-byte block. [confirmed]

The main bcall table pins mode 6 to the App-restriction API: [confirmed]

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

certificate_tail.restriction_control occupies certificate offset 0x1DD2. The 13-byte restriction_record field at 0x1DD30x1DDF acts as a record or as an App-restriction bitmap, depending on the API operation. For an App on Flash page $p$, the bitmap index is $p - 8$. 3D:7D69 divides that index by eight, and the mask helper at 3D:785D uses least-significant-bit-first ordering. A clear bitmap bit means that the App is restricted. [confirmed]

The low control-byte bits have these ROM-confirmed roles:

BitMaskClear-bit meaningEvidence
00x01Base restriction control is active.The type-2 set and query paths at 3D:7C02 and 3D:7CD8; aggregate type 3 is queried by _ExecutePrgm at 07:5758, while equation/token paths query type 2.
10x02logBASE is disabled.Type 6 selects mask 0x02 at 3D:7CE3; the UI string at 37:4A42 and query at 37:4E43 name logBASE.
20x04The summation token is disabled.Type 7 selects mask 0x04 at 3D:7CDD; the UI string at 37:4A54 and query at 37:4E52 name the summation token.

The API dispatch gives each restriction type the following behavior:

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

_SetAppRestrictions accepts types 04, 6, and 7; it rejects type 5. _RemoveAppRestrictions accepts types 1, 2, 3, and 6. Removal loads the 14-byte span into 0x8479 at 3D:7DCE. The rebuild wrapper at 3D:7D82 invokes mode 6 and writes the updated span back. Set paths clear Flash bits with direct programming. Removal restores some cleared bits to one, so it rebuilds the complete 0x216-byte certificate tail. [confirmed]

Mode 5 sets a per-App validity bit. locate_app_validity_bit at 3D:51F6 starts with _GetCertificateStart + 0x1FE0, divides the calculated App index by eight at 3D:7D6B, and retains the low three bits as the bit index. set_app_validity_bit at 3D:51BE advances past 0x1FE0 before reading, so the bitmap starts at half-relative offset 0x1FE1. The mask loop at 3D:785D uses least-significant-bit-first ordering. [confirmed]

The receive path from _WriteToFlash at 3D:6DA5 reaches the set routine at 3D:70E1. If the bit is clear, stage_app_validity_byte at 3D:51A6 updates the 0x836D tail buffer and calls mode 5 at 3D:51D7. Setting a NOR Flash bit from zero to one requires the erase-and-rebuild path. The inverse routine, clear_app_validity_bit at 3D:51E4, masks the bit to zero and reaches _WriteAByte = 8021h through 3D:7CB3; programming one to zero does not require an erase. [confirmed]

The boot bcall table and page-3F bodies independently confirm the OS-validity bit in certificate_tail.validity[0], at offset 0x1FE0: [confirmed]

BcallIDBodyBehavior
_MarkOSInvalid8093h3F:5209Stage 0x1F180x1FFF, set bit 0 in the staged 0x1FE0 byte at 0x836D, and erase/rebuild the certificate data.
_MarkOSValid8099h3F:51F5Read 0x1FE0, clear bit 0, and program the byte through _WriteAByte = 8021h.
_CheckOSValidated809Ch3F:52C6Read 0x1FE0 and test bit 0.

Bit 0 clear means that the OS is valid; bit 0 set means that it is invalid. WikiTI gives the same field label, but the conclusion above comes from the boot ROM paths. WikiTI also labels certificate_tail.gc_recovery at 0x1DEA as garbage-collection information. The mode-3 and mode-4 call chains independently confirm that the 0x1DEA0x1E4F block stores garbage-collection recovery metadata. The exact meaning of every byte is not established: the first six fields and the live sector-state array are decoded, while the two retained trailing bytes at 0x1E4E0x1E4F have no direct semantic accessor. [confirmed] for block ownership and access bounds; [hypothesis] for the trailing bytes’ owner.

On the TI-84 Plus path, 3C:7E6B loads the existing block and 3C:7317 writes 0xFF over only the first 0x64 bytes in RAM at 0x82A5. Mode 4 later rebuilds the full 0x66 bytes from that buffer, retaining offsets +0x64 and +0x65. The first six initialized bytes are fixed fields, leaving 94 bytes of state-array capacity; the TI-84 Plus archive limit 0x2A makes only slots 08 live. [confirmed]

tools/certificate_rebuild.py exposes the signature-checked reconstruction as a library. Its thin CLI reports the block partition, all seven branches, direct and bjump invocations, resolved mode owners, OS/App-validity metadata, and App-restriction behavior:

python tools/analyze_certificate_rebuild.py --json

tools/gc_journal.py decodes the 0x1DEA recovery block, master phase dispatch, and archive-sector state indexing. Its CLI can correlate the static ROM paths with state-changing command writes in a TilEm trace. See Variables, archive and unarchive for the field and phase tables. [confirmed]

python tools/analyze_gc_journal.py --json
python tools/analyze_gc_journal.py --trace /tmp/tibasic-smoke/gcflash.trace

The reusable call analyzer can resolve a banked target back through its page-0 bjump stub and report candidate callers with linear-disassembly context:

nix develop -c python tools/analyze_rom_calls.py \
  3D:4771 --bjump-call --before 5 --after 5

The complete-ROM raw scan and linear disassembly independently find 90 D3 14 occurrences. All 90 use one of four privilege-sequence spellings: 70 load A=1, and 20 clear A. Page 3D contains 34 unlock forms and the shared lock form at 3D:5CE6. Page 3D never writes port 0x21. Its only resolved port-0x21 access is the read at 3D:7392 that selects a model-specific certificate page. These paths do not change the modeled physical-sector override. [confirmed] for the ROM scan; [standard] for the emulator-defined override role.

tools/flash_gate.py exposes the raw scanner as a library. The thin CLI keeps complete privileged sequences separate from unmatched D3 14 candidates:

python tools/analyze_flash_gate.py --page 0x3D --json

The two program workers share the command writes, LDI, DQ7/DQ5 polling, and reset write. A sequence comparison aligns 116 bytes. Five byte spans encode the differences: [confirmed]

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

The page-3D caller ignores the worker flags after 3D:4332. A DQ5 failure therefore does not stop its byte-copy loop. Even a caller that inspects the flags cannot treat Z as unconditional success: the failure tail writes 0xF0 at the target, pops the saved port-0x06 value into A, restores that page, and executes OR A. A saved page zero produces Z on the failure path. [confirmed]

The guarded certificate-program-error fixture copies the unmodified 129-byte worker to 0x8100, saves page zero, and requests 0x80 over stored 0x00 at 3E:4000. It runs only with the patched unlock wrapper and verifies the worker head, worker tail, and target byte before programming. Its machine-code SHA-256 is 34fc6b71a0015cbcb13578a30ec195883a187ee43b234d6ab671d00275824429. [confirmed]

Pinned TilEm returns program-status values 0x00, 0x60, and 0x20, then executes the failure reset at ram:817B. The trace decoder labels the invocation certificate-failure. The copied worker returns AF=0x0044, Z, with BC=0, DE=0x4000, HL=0x9E63, and port 0x06 restored to zero. The final target remains 0x00. This dynamically confirms the worker tail in TilEm; physical DQ5 behavior remains unmeasured. [confirmed] for the ROM and TilEm trace; [hypothesis] for hardware.

tools/flash_workers.py extracts two-byte-length descriptors and compares worker bytes. Its CLI reproduces the lengths, hashes, aligned-byte total, and five edit spans:

python tools/describe_flash_workers.py --json

Locked write can satisfy DQ7 under TilEm

The Flash APIs do not unlock the ASIC command gate. A caller can therefore reach the unmodified worker while port 0x14 still blocks every command write. The worker checks only DQ7 during the normal completion path. It does not compare the remaining seven data bits after DQ7 agrees. [confirmed]

The read-only locked-byte-noop fixture verifies the 16-byte _WriteAByte wrapper signature and the 16-byte protected lock-wrapper signature. It calls the original lock wrapper at 3C:66D5, then aborts unless port 0x02 bit 2 is clear. The fixture uses the unmodified ROM and restores the original OP1 byte. Its machine-code SHA-256 is 4a843bc617282b44c5a1dac1c6f08627c65c33175908198c908bddc8ba4b82ee. [confirmed] for the fixture construction.

The source byte at 3D:7FFF is 0x50. The fixture requests legal NOR programming to 0x40; both values have DQ7 clear. TilEm reports port 0x02 as 0xE3 before the call, confirming that its Flash-unlocked bit is clear. The trace records five CPU write attempts targeting mapped Flash: [confirmed] for TilEm execution.

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

TLMT records CPU writes to the mapped Flash window, not whether the ASIC or device accepted them. The command decoder consequently recognizes one command-shaped byte-program sequence and one reset. The final array read is the acceptance check: 3D:7FFF remains 0x50. Port 0x02 also remains 0xE3. [confirmed]

The bcall returns AF=0x0044, Z, with BC=0, DE=0x8000, HL=0x8479, and OP1=0x40. The return state is indistinguishable from a completed one-byte worker call unless the caller verifies array data. This confirms the caller’s unlock obligation in pinned TilEm and shows another path where Z does not prove mutation. It does not establish how a physical ASIC handles the same attempt. [confirmed] for the ROM and emulator trace; [hypothesis] for physical behavior.

Cross-page destination behavior

The intended path uses a RAM source, so source H has bit 7 set. On that path the worker detects DE > 0x7FFF, increments the current target page, and resets DE=0x4000 before the next byte. [confirmed]

The ordinary Archive prgmA trace groups its 17 byte-program commands into six worker invocations. The garbage-collection window groups 1,133 commands into 56 invocations, with a maximum length of 232 bytes. Every one is page-local, physically contiguous, and followed by a reset at its final target. These ordinary paths therefore exercise the worker but do not by themselves test its page-crossing branch. [confirmed]

A deliberate TilEm trace archives a generated 17,000-byte prgmZBIGDATA through MEM > Mem Mgmt/Del > Prgm. One _WriteFlashUnsafe invocation programs all 17,002 bytes of the variable data, from physical 0x20013 (08:4013) through 0x2427C (09:427C). The decoder observes exactly one 08:7FFF to 09:4000 crossing, no discontinuity, and the terminal 0xF0 reset at 09:427C. At the crossing, ram:811B reads port 0x06 with A=0x08 at clock 230,976,551; ram:8122 outputs A=0x09 at clock 230,976,580; and ram:8124 has reset DE from 0x8000 to 0x4000 at clock 230,976,590. This confirms the ordinary 08 to 09 software path in emulation; it is not a physical-calculator Flash test. [confirmed]

The boundary code contains a page-3E quirk:

in a,(0x06)
inc a
cp 0x3e
jr z,skip_out
out (0x06),a
skip_out:
ld de,0x4000

A write that crosses from page 3D computes page 3E but skips the page-select output. It resets DE to 0x4000 and continues on the old mapping. This is not a clean stop at the certificate boundary. Starting _WriteFlashUnsafe on page 3E can increment toward page 3F; the hardware protection layer remains separate. [confirmed]

An emulator-only TilEm fixture exercises the page-3D boundary with A=0x3D, DE=0x7FFF, BC=2, and RAM source bytes 0x40,0xE0. It patches only the tail of a protected page-3C unlock wrapper in a copy of the exact OS image. The copied flash_program_worker_code remains unchanged. The generated assembly program checks all eight patched bytes and exits on an unmodified ROM before it can unlock Flash. [confirmed] for the fixture construction.

The trace decodes two byte-program commands followed by one array reset: [confirmed] for TilEm behavior.

ClockCommandPhysical targetValue
186,446,349byte program3D:7FFF (0xF7FFF)0x40
186,446,829byte program3D:4000 (0xF4000)0xE0
186,447,016array reset3D:4000 (0xF4000)0xF0

At clock 186,446,607, ram:811B reads port 0x06 while the mapping is page 3D. ram:811D increments the value to 0x3E; ram:811E compares it with 0x3E; and ram:8120 takes the zero branch. The trace contains no execution of the page-select output at ram:8122. At clock 186,446,640, ram:8124 has set DE=0x4000 while page 3D remains mapped. The trace decoder classifies the physical 0xF7FFF0xF4000 transition as same-page-window-wrap. [confirmed]

This run confirms the static branch in TilEm. It does not test the physical ASIC, the photographed Fujitsu device, or a production ROM without the emulator-only unlock shim. The fixture and commands are documented under “Guarded Flash-worker fixtures” in the repository’s tools/dynamic-tracing.md.

Illegal byte-program failure under TilEm

A second guarded fixture calls _WriteFlashUnsafe with A=0x3D, DE=0x7FFF, BC=1, and source byte 0xD0. The source ROM holds 0x50 at 3D:7FFF, so bit 7 requests an illegal NOR 0→1 transition. The fixture uses the same eight-byte unlock shim guard as the page-3E probe. Its machine-code SHA-256 is d83208e1bbcc0f891b2bb73f7558cc521d55c37ce91c3ebdd88b5076e04c5076. [confirmed] for the fixture construction.

TilEm f56ad63’s emu/flash.c applies stored &= requested and enters FLASH_ERROR when the stored byte does not equal the request. Error reads complement the requested DQ7, set DQ5, toggle DQ6, and leave the error state active until reset. The pinned file’s SHA-256 is 280e0e45b6e1f1ef21d779abb809eaef2d04d08db09feb87a459e079280c9545. [standard]

The trace records this poll sequence: [confirmed] for TilEm behavior.

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

The bcall returns to ram:9DBE with AF=0x3F2C at clock 186,668,984. The fixture remaps page 3D, rereads 3D:7FFF, and observes the unchanged stored byte 0x50 at clock 186,669,043. The trace decoder uses the reset-write PC to label this invocation worker_outcome: "failure". [confirmed]

This result confirms how the unmodified worker responds to TilEm’s persistent program-error state. It does not measure status timing, DQ bits, or failure recovery on the photographed Fujitsu device or another physical calculator.

Source-space branch and ROM callers

If source H has bit 7 clear, the worker sets (IY+0x25).1 and skips destination-crossing logic. An exhaustive raw-bcall scan of the retail ROM finds 20 _WriteFlashUnsafe (8087) candidates and three _WriteFlash (80C9) candidates. Static register reduction puts every source in RAM: [confirmed]

PageBcall sites and source HL
365E5C=82A5
3C630E=8000, 6AA0=82A5, 6AF5=983A
3D436C=8478, 4670=9C9E, 5050=82A5, 5852=8000, 58ED=8478, 5926=8478, 5CBA=83A5, 6522=83F9, 6578=8478, 65BA=(83F3), 6A23=83FD, 6A39=8402, 6AA6=8000, 6ACE=8000, 718C=8000+offset, 71E1=8479, 7201=8000, 7ABB=8479, 7B72=983A

The page-3C site at 3C:6AF5 is _WriteFlash (80C9h). The flush_paged_flash_block caller at 3C:6AB1 loads HL=0x983A, B=0, and C=(0x9834) after opening the port-0x14 gate. It accepts model-dependent pages only after the classifier at 3C:6B79; the TI-84 Plus range is 0x080x29. The link receiver reaches this staging path only when the destination loaded at 3C:42AB has bit 15 clear. RAM destinations take the direct store at 3C:42D4. The second mode-3 owner is the USB receive-to-memory loop at 36:40E7. It fills 0x983A through the page-35 endpoint helper at 35:4FA1, which reads port 0xA1 at 35:500E, then calls the page-3C dispatcher at 36:415C. [confirmed]

At 3D:65BA, the source is arcInfo.dest_ptr. Setup at 07:6331 saves the incoming data pointer from the variable lookup in that field. It is a RAM data pointer on the RAM-to-Flash path; the Flash-to-RAM path later replaces it at 07:622E with the newly allocated RAM destination. The GC trace reaches this bcall twice with HL=9E53; the normal archive trace reaches it once with HL=9E21. The helper called before 3D:718C returns either 8000 or 8000+offset within its caller’s established range. The local helpers at 3D:5258 and 3D:5964 preserve source HL for their dependent sites. [confirmed]

The copied-worker entry provides an independent runtime check. Opcode E6 at ram:8100, with destination DE<8000, identifies block-program entries. The GC trace contains 62: source HL is 8000 once, 83F9 twice, 83FD once, 8402 once, 8478 55 times, and 9E53 twice. The normal archive trace contains six: 83F9 once, 8478 four times, and 9E21 once. No observed entry takes the H<80 branch. [confirmed]

The alternate branch is not a general Flash-to-Flash copy path. The worker selects the destination page through port 0x06 before it reads the source. A source in the banked 40007FFF window therefore aliases the destination page rather than retaining an independent source page. A source in the fixed 00003FFF window can still be read. The guarded low-source-cross fixture tests that case on an unmodified ROM. It locks Flash through the protected wrapper at 3C:66D5, confirms port 0x02 bit 2 is clear, and calls _WriteFlashUnsafe with A=0x3D, DE=0x7FFF, BC=2, and HL=0x0068. Source bytes 00:0068 and 00:0069 are 0x4D and 0x50. [confirmed]

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

The bcall returns AF=0x0044, Z, with the final BC, DE, and HL values shown above. The probe captures RAM 8000=F0, (IY+0x25).1 set, 3D:7FFF=50, and port 0x02=E3 before restoring its RAM and flag fixtures. The Flash decoder sees one command-shaped byte-program attempt and no Flash reset because the terminal reset resolves to RAM. The fixture ROM hash equals the source ROM hash; the probe’s machine-code SHA-256 is bb8159803d67bbfdc354d523db7dbe72e02bf4469a89c79d2c7d033dd660074e. [confirmed] for pinned TilEm and the unmodified ROM.

The branch remains unused by every statically identified ROM call and both available OS write traces. Its intended external use, if any, remains unknown. The dynamic result does not establish what a physical ASIC and Flash device do with the locked command-shaped write attempt. [hypothesis] for a use outside the documented RAM-source ABI and for physical consequences.

Erase APIs and worker

_EraseFlashPage sets HL=0x4000, masks A to six bits, and rejects page 3E. Its equality comparison returns A=0x3E, Z on that no-op path. For page 00 it changes HL to 0x0000, because page 0 is fixed below the banked window. It then falls into _EraseFlash. [confirmed]

_EraseFlash applies the same immediate-return-address check as _WriteFlashUnsafe. A direct caller with a return address at or above 0x8000 returns NZ before worker launch. A bcall proceeds to copy the erase worker to 0x8100. The routine does not reject page 3F; the Flash chip’s sector protection is a later, independent gate. [confirmed]

Erase-entry trace

The read-only erase-entry-returns fixture verifies eight bytes at each of 3F:4C1E, 3F:4C2A, and 3F:4E3F. It never writes port 0x14, and every test returns before worker launch. The trace contains zero resolved Flash writes. [confirmed] for the fixture and TilEm execution.

ClockCall and triggerReturn AFCondition
187,400,702_EraseFlashPage, input page 0x7E → masked page 3E0x3E42Z
187,400,886direct CALL 3F:4C2A from RAM, input A=0xA50xA591NZ
187,402,383_EraseCertificateSector, HL=0x5000, seeded AF=0xA5450xA545caller value

The page rejection returns at 3F:4C25. The direct call returns at 3F:4C2E. The invalid certificate address branches from 3F:4E4E to the common tail at 3F:4E553F:4E56:

POP AF
RET

[confirmed]

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

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

It polls target DQ7 until it becomes 1. If DQ5 becomes 1 first, it takes the failure path. Success forces port 0x06 to page 3F and returns A=0, Z. Failure loads A=0xF0, writes it through DE, executes OR 1, forces page 3F, and returns A=0xF1, NZ. The write through DE is present in the copied worker even though _EraseFlash documents only A and HL as inputs. [confirmed]

Failure-path DE audit

The RAM-worker launcher at 3F:48C5 saves the caller’s BC, DE, and HL, copies the worker, restores those registers, and only then calls 0x8100. It does not synthesize a reset pointer. The worker therefore receives whatever DE the caller supplied. [confirmed]

The Fujitsu command table defines 0xF0 as a read/reset command accepted at any Flash address. Writing it through a Flash pointer is consequently a valid way to leave the status state and return to array reads. [standard] The ROM code does not check that DE is such a pointer. If DE >= 0x8000, the same instruction writes 0xF0 to RAM instead; the public _EraseFlash ABI does not document DE as an input. [confirmed] for the conditional ROM behavior; [hypothesis] for a physical test that deliberately reaches DQ5 with a RAM pointer.

There is one raw _EraseFlash bcall sequence in the OS image, at 3D:45EA. The wrapper at 3D:45E7 first selects the model-specific certificate page, then calls bcall 8024. Four direct calls reach that wrapper on page 3D: [confirmed]

CallDE evidence at the erase
3D:40A3The helper at 3D:409F and boot _GetCertificateStart preserve incoming DE. Its 3D:7A68 caller leaves DE=0x1DE2, a fixed-page Flash address. The 3D:71C3 path instead carries a metadata word returned by 3D:5B17, not a pointer.
3D:4252_GetCertificateStart at 3D:424D, followed by EX DE,HL, puts the active certificate-half start in DE.
3D:60EEThe local branch toggles HL to the half being erased while preserving caller DE. Page-0 thunk 00:3EEB reaches this routine from reset at 00:0D73 before any local DE initialization, and from two page-37 call sites without an ABI constraint on DE.
3D:6127The alternating scan starts with HL=0x4001, DE=0x6001; on exit DE still points into the other certificate half.

The 3D:71C3 provenance is byte-specific. 3D:787B calls 3D:5B17, which saves a scan-result word, fetches an OS-header subfield through _FindOSHeaderSubField, reads its first byte, then forms returned DE from that byte and the saved scan count. The caller requires returned D to be nonzero, saves DE at 3D:7089, and restores it at 3D:71B2 immediately before the erase branch. _GetCertificateStart preserves DE with explicit push/pop pairs at 3F:486E3F:4884. This path therefore transports archive and OS-header metadata through the erase call; it does not establish a Flash reset pointer. [confirmed]

The 3D:60EE entry is similarly caller-controlled. Its page-0 thunk at 00:3EEB contains this raw descriptor:

CALL 2B09
.dw 6098
.db 7D

Masking the raw page selects physical page 3D. The reset caller initializes HL, SP, and IY at 00:0D6500:0D6F but not DE before calling the thunk. The page-3D body and its Flash-byte reader preserve that inherited value through the erase at 3D:60EE. [confirmed]

The GC trace supplies a counterexample to any broader internal convention. Its seven entries at 3F:4C2A carry DE=0x802C, 0x802C, 0x802C, 0x6000, 0x7DF1, 0x4001, and 0x4000. The first three are RAM pointers. All seven erases succeed, so none reaches 3F:4C83; nevertheless, the launcher would preserve the same DE values for the failure path. [confirmed]

Thus multiple certificate-wrapper paths intentionally or incidentally provide a Flash address suitable for the reset command, but the ROM as a whole has no such invariant and the convention does not extend the public ABI. The failure path is best classified as an underspecified interface with a conditionally unsafe RAM write. [confirmed]

The 0x30 command erases a physical sector, not one logical page. _EraseFlashPage is therefore named for the page used to select a sector, not for 16 KiB erase granularity. [standard]

Certificate sectors

_EraseCertificateSector preserves AF around its work. It accepts only H=0x40 or H=0x60; other values return without erasing. For either accepted address, it loads A=0x3E, calls _EraseFlash, restores the caller’s AF, and returns. The restored flags hide both the Z success and NZ failure result from _EraseFlash. The two values select the two 8 KiB sectors within physical page 3E. [confirmed]

Successful certificate erase under TilEm

The guarded certificate-erase-success fixture runs only on the patched ROM copy. It unlocks Flash, seeds AF=0xA545, and calls _EraseCertificateSector with HL=0x4000. The source image contains 0x00 at physical 0xF8000, so the post-erase read distinguishes mutation from an already erased byte. The fixture machine-code SHA-256 is e46ffebe8dbeb6a37ea62790744e8d758dc4772b08046b058ed4a0f351dee97e. [confirmed] for the fixture construction.

The trace resolves all six command writes and decodes one sector erase at 3E:4000 (0xF8000) at clock 186,869,906. The selected physical sector is 0xF80000xF9FFF, matching the first 8 KiB certificate half. [confirmed] for TilEm execution of the ROM command path.

The worker reads the target at ram:8138 24,497 times. Grouping the observed A values separates TilEm’s two modeled erase phases: [confirmed] for the trace values; [standard] for the pinned TilEm state names.

TilEm stateTarget-read valueCount
FLASH_BUSY_ERASE_WAIT0x003
FLASH_BUSY_ERASE_WAIT0x443
FLASH_BUSY_ERASE0x0812,245
FLASH_BUSY_ERASE0x4C12,245
array data after completion0xFF1

The first reads occur at clocks 186,869,913 (0x00) and 186,869,962 (0x44). Active-erase values begin at clock 186,870,207. The final 0xFF read occurs at clock 188,070,217. ram:8143 then takes the success path at clock 188,070,241, and the worker returns A=0, Z at ram:8151. [confirmed]

3F:4E55 restores AF=0xA545 at clock 188,070,415. The bcall-visible result at ram:9DBA remains 0xA545, and the fixture rereads 3E:4000 as 0xFF at clock 188,070,584. This dynamically confirms that the certificate wrapper hides the successful worker result. It does not establish physical erase duration, status cadence, or wrapper behavior on another OS image.

The garbage collector uses those halves as a transactional certificate and phase-journal pair. It erases the inactive half, copies the used tail of the active half, switches the active marker, and later copies the tail back. This behavior is visible as separate erases at physical 0xF8000 and 0xFA000; it does not treat page 3E as one 16 KiB erase unit. [confirmed]

Erase-busy read scope under TilEm

The guarded erase-busy-range fixture issues the sector-erase command directly for 3E:4000 (0xF8000). It waits for DQ3 before sampling the selected sector, nearby top-boot sectors, and a distant archive page. The fixture then waits for DQ7 and reads the same locations in array mode. Its machine-code SHA-256 is 561c424816f0dd4dbe76cba7635d2edabb433a234860e63c1c8767dab8254781. [confirmed] for the fixture construction.

The trace decodes one sector erase at clock 187,143,123. TilEm returns its alternating active-erase values at all six sampled addresses: [confirmed] for TilEm execution.

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

Only physical 0xF80000xF9FFF is erased. The final values at the adjacent, preceding, boot, and distant samples match the source ROM. [confirmed]

Pinned TilEm handles FLASH_BUSY_ERASE before applying an address-dependent read result. It warns when the read and erase-command addresses have different upper 16 physical-address bits, but returns erase status after either outcome. The run emits one reading from Flash while erasing warning for 0x20000. The other five addresses share upper byte 0x0F with the erase target, so the warning check does not distinguish their physical sectors. [standard] for the pinned source; [confirmed] for the trace and warning.

The Fujitsu data sheet gives different address scopes to the status bits. DQ6 toggles on successive reads from any address, while DQ2 toggles only when read from an erasing sector. It also requires DQ7 erase polling within a selected sector. TilEm’s global 0x08/0x4C alternation therefore models DQ2 outside the selected sector more broadly than the data sheet specifies. [standard] Physical TI-84 Plus behavior at these boundaries remains unmeasured.

_SetFlashLowerBound bcall

_SetFlashLowerBound = 80CFh takes the new bound in A. The official name is misleading on the TI-84 Plus: the body writes port 0x23, which is the upper end of the modeled forbidden Flash-execution interval. It does not program or erase the Flash array. Its complete body is: [confirmed]

3F:4784  nop
3F:4785  nop
3F:4786  im 1
3F:4788  di
3F:4789  out (0x23),a
3F:478B  di
3F:478C  ret

The leading bytes form the protected-port sequence. Flash must already be unlocked for port 0x23 to accept the write. The routine preserves A, the flags, and the other general registers. It selects interrupt mode 1 and leaves maskable interrupts disabled. A caller must restore its prior interrupt-enable state and must already accept IM1 as the OS interrupt mode. [confirmed] for the routine; [standard] for the write gate.

The executable probe writes the boot default upper bound:

    ld a,$2A
    rst $28
    .dw $80CF

The guarded runner requires port 0x23 = 0x2A and IFF2 clear after the call. [confirmed] for pinned Wabbitemu execution.

This wrapper records IFF2 through LD A,I, calls the bcall, then conditionally restores interrupts. POP AF also restores the caller’s original AF:

    ld a,i
    push af                     ; P/V records the prior IFF2 value
    ld a,0x2A
    rst 0x28
    .dw 0x80CF                  ; _SetFlashLowerBound; returns with DI
    pop af
    jp po,interrupts_restored   ; prior IFF2 was clear
    ei
interrupts_restored:

The example assumes that trusted code already opened the protected-write gate and will close it. See Execution protection for the cross-emulator boundary comparison.

Archive allocation above the hardware API

The archive manager and the raw Flash API solve different problems. The boot bcalls program an address supplied by their caller. Page-3D code chooses an archive record location, maintains record states, and invokes the boot API. [confirmed]

Dynamic archive boundary

The archive pool begins at page 08. Its upper boundary is computed around installed Flash Apps; it is not the fixed range 0x150x1E. [confirmed]

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

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

At each candidate it reads the first byte at logical 0x4000. A possible App header (0x80 or 0x00) is validated through the page-3C helper reached at ram:3DC5; _FindAppNumPages at 3D:4AA3 then returns the App span in C. The routine subtracts that span and repeats. It returns the first page below the installed App run in B. [confirmed]

3D:62C2 stores that value as an exclusive upper bound, loads A=0x08, and scans archive records upward from 08:4000. Its page comparisons stop at or above the dynamic bound. With no installed Apps in the local image, the trace returns B=0x29 and selects A=0x08, HL=0x4000 for the new record. [confirmed]

The nearby selector at 3D:738B returns 0x1E, 0x3E, or 0x7E. Those are model-specific certificate pages. They do not define the archive pool’s upper endpoint. [confirmed]

Record writer

3D:64AA is the archive record writer. It unlocks Flash, checks or retires the previous record marker, writes 0xFE, programs the size, symbol header, name, and data, then changes the record status to 0xFC. It calls _WriteAByte for marker bytes and _WriteFlashUnsafe for blocks. [confirmed]

The checks at 3D:6B6D and 3D:6B9B reject pages below 08, reject pages at or above the dynamic App boundary, and require the Flash destination to be at least 0x4000. The block form at 3D:6B6D also requires its RAM-side address to be at least 0x4000. [confirmed]

Record state changes only clear bits, matching NOR programming rules: 0xFF is erased, 0xFE is in progress, 0xFC is complete, and 0xF0 is retired. Sector erase is the only operation that restores zero bits to one. See Variables, archive & unarchive for the record layout and garbage collector. [confirmed]

End-to-end archive trace

tools/macros/archive-program.macro cold-boots the calculator, creates prgmA, inserts one token, and executes Archive prgmA. The final screen is Archive prgmA followed by Done. The trace contains 4,015,092 instructions, 19,876 mapping writes, and no unresolved mappings. [confirmed]

The executed write path is: [confirmed]

07:6107  archive RAM-to-Flash path
  → 3D:61AF
  → 3D:62C2  free-record scan; selects 08:4000
  → 3D:64AA  archive record writer
      → 3F:4C9F  _WriteAByte, three calls
      → 3F:4CA6  _WriteFlashUnsafe, six calls total
          → 0x8100  copied byte-program worker

The calls write an initial 0xF0 marker when needed, 0xFE, a two-byte size field, an eight-byte header, a four-byte payload, and final status 0xFC. Every boot-worker call follows the successful DQ7 path and returns A=0. [confirmed]

The trace also resolves the archive-range ambiguity directly. 3D:6413 returns B=0x29; 3D:62C2 explicitly starts at page 08; and the programmed physical target is page 08. [confirmed]

The generated large-program trace uses the same record path and demonstrates that the data block is not split at a 16 KiB page boundary: its 17,002-byte worker invocation crosses contiguously from page 08 to page 09. The fixture builder, UI macro, decoder, and exact worker-point query are documented under “Cross-page Flash-programming fixture” in the repository’s tools/dynamic-tracing.md. [confirmed]

End-to-end garbage-collection trace

The generated GCFLASH program archives real variables A and B, unarchives A, and runs GarbageCollect. The macro selects 2:Yes at the confirmation prompt. Dynamic coverage reaches gc_command at 3C:71F8, archive_gc_collect at 3C:7733, and the boot erase body at 3F:4C2A. [confirmed]

The decoded GC window contains 4,630 Flash writes. They form 1,133 AMD byte-program commands, seven sector erases, 56 array-reset writes, and no unmatched command writes. The physical erases occur in this order: [confirmed]

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

The page-0C erase and page-08 erase each cover four logical pages. The page-3E erases cover one 8 KiB half each. The command sequence therefore directly confirms that the collector follows the physical top-boot geometry rather than issuing one erase per 16 KiB paging unit. [confirmed]

The collector uses page 0C as the destination for the surviving B record, retires the old record at 08:4016, erases the old page-08 sector, and marks page 08 as the next empty scratch sector. It copies the used certificate tail between the two page-3E halves while persistent phase bytes advance. [confirmed] See Variables, archive and unarchive for the record bytes, sector-header states, journal fields, and recovery dispatcher.

tools/hardware_trace.py exposes reusable resolved-instruction and resolved-memory-write iterators. tools/flash_trace.py decodes AMD commands and groups adjacent program runs. Their focused CLIs reproduce the phase timeline without parsing the binary trace in a one-off script: [confirmed]

python tools/analyze_flash_trace.py \
  /tmp/tibasic-smoke/gcflash.trace \
  --clock 321347460-344829074 \
  --timeline

python tools/analyze_trace_points.py \
  /tmp/tibasic-smoke/gcflash.trace \
  --point page_3C:7733 \
  --point page_3C:7cfb

The same decoded command stream can be replayed into immutable Flash images at active journal phases. Cold TilEm boots now exercise all six phase-dispatch branches. The 0xFF, 0xFE, 0xFC, 0xF8, and 0xE0 replays converge byte-for-byte with uninterrupted execution. The 0xF0 replay has identical archive bytes but completes certificate cleanup one boot earlier; cold-booting the uninterrupted result once produces the same stable image. See Variables, archive and unarchive for the input and trace hashes, command counts, controlled-topology boundary, and deferred-cleanup result. [confirmed] for TilEm.

Pinned Wabbitemu cold boots independently execute the same six dispatcher branches. Complete output images equal the corresponding TilEm recovery results. The record-authentic 0xF0 input is reconstructed from eight deterministic program records before the unmodified OS materializes its journal phase. See Variables, archive and unarchive for input hashes, dispatcher visits, and changed-byte counts. [confirmed] for the emulator command-boundary runs.

Reproduce the trace

Use the repository’s Nix environment when z80dasm or another analysis utility is not installed globally. The trace itself is large, so it is generated outside the repository. [confirmed]

TILEM=~/Git/tilem-headless/result/bin/tilem2

$TILEM --headless --rom tools/rom.bin --model ti84p --normal-speed --reset \
  --macro tools/macros/archive-program.macro \
  --trace /tmp/tilem-archive-program-success.trace --trace-range all

tools/tilem_trace_resolve.py /tmp/tilem-archive-program-success.trace \
  --initial-mapping ti84p-reset --coverage --sort addr \
  --names tools/names.txt

nix develop -c z80dasm -a -t -g 0x4000 \
  /tmp/ti84-page3f.bin

See tools/dynamic-tracing.md for page-resolution details and trace-format caveats.

Emulator comparison

The four inspected emulators agree on the command bytes and top-boot sector boundaries used by the ROM. They differ at the points most useful for negative tests: illegal bit transitions, completion timing, status reads, and ASIC access control. [standard]

BehaviorTilEm f56ad63Wabbitemu 48c2dc0MAME 0.287jsTIfied 20170706a
Unlock addresseslow 12 bits 0xAAA, 0x555low 12 bits 0xAAA, 0x555accepts several AMD address conventions, including the ROM’s low-12-bit formlow 12 bits 0xAAA, 0x555
Byte mutationold &= requestedold &= requestedold = requestedold &= requested
Successful program7 µs real-time timer; 42 clocks at the 6 MHz reset speedimmediate array dataimmediate array dataimmediate array data
Illegal 0→1 requesterror stateone transient error readwrites the requested one bitleaves the zero bit unchanged without an error state
Sector erase50 µs command window, then 200 ms erase timer; 300 and 1,200,000 clocks at 6 MHzimmediateimmediate data mutation followed by a timerimmediate; protected sector-table entries are skipped
Autoselectincompletemodeled AMD manufacturer 0x01, device 0xDAIDs at offsets 0/1; no compatible protection readmanufacturer 0xC2 and device 0xDA; each recognized read exits ID mode
Chip erasewritable sectors only; final status follows the last sectorimmediate full-array fill, including bootimmediate full-array fill; stale/default busy rangeimmediate erase of unprotected sector-table entries
Fast programcommand flow present; fidelity unresolvedimplemented for TI-84 Plus Flash version 3entry accepted, but A0 excludes the AMD maker IDabsent
Erase suspend/resumeabsentabsentabsentabsent
CFI queryabsentabsentabsent for AMD_29F800Tabsent
Sector-protection autoselect readunavailable with missing autoselectoffset 4 always returns zerono data-sheet-compatible protection readabsent
ASIC write gateprotected-byte sequence, lock, and sector groupsprivileged-page port-0x14 gate and boot-page flagsno effective Flash-write gateprotected-byte port-0x14 gate; sector flag affects erase but not program

The table combines source results with guarded runtime checks described below. MAME marks the complete TI-84 Plus driver MACHINE_NOT_WORKING. None of the divergences resolves physical behavior. [standard] for the source models; [confirmed] for the pinned runtime observations.

The emulator autoselect rows differ from the photographed Fujitsu part’s data sheet, which specifies 0x04/0xDA. Matching device code 0xDA establishes a compatible top-boot command family; manufacturer 0x01 does not identify the photographed package. [standard]

TilEm behavior and limits

TilEm implements the same command progression used by the ROM: AA, 55, then A0 for program, or 80, AA, 55, 30 for sector erase. It matches command addresses by physical low 12 bits 0xAAA and 0x555. [standard]

Its program operation computes stored_byte &= requested_byte. A requested 0→1 transition leaves the zero bit unchanged and enters the emulator’s error state. During program busy, DQ7 is complemented and DQ6 toggles. The delay argument is 7 µs, not seven CPU cycles. TilEm’s real-time scheduler converts it to 42 clocks at the 6 MHz reset speed. [standard]

During erase, DQ6 and DQ2 toggle. DQ3 distinguishes a 50 µs command window from the modeled 200 ms erase operation. Those deadlines are 300 and 1,200,000 clocks at 6 MHz. The ROM’s erase worker polls DQ7 and DQ5 rather than those toggle bits. [standard]

TilEm’s source comment lists fast program among unfinished work, but the state machine implements part of it. AA 55 20 enters fast mode, A0 selects one program operation, and the next write calls the ordinary byte-program helper. The state then returns to fast mode. 90, followed by F0, exits. This is an implemented command flow with unresolved hardware fidelity. Autoselect logs that it is unimplemented; erase suspend and CFI have no states. [standard]

TilEm’s chip-erase path iterates over the sector table and calls erase only for sectors accepted by its protection model. On a TI-84 Plus with default override group zero, this skips physical 0xB00000xBFFFF and 0xFC0000xFFFFF. Override group one admits those sectors. Each sector erase resets the recorded program address and timer, so the final busy status describes only the last writable sector. This differs from a single physical chip-erase operation. [standard]

A guarded direct-core run exercises these states through tilem_flash_write_byte and tilem_flash_read_byte. It seeds synthetic memory and enables TilEm’s delay model. The timer deadlines come from the scheduler; the fixture invokes the registered Flash callback directly to cross each deadline without executing TI-OS. [confirmed]

Program caseState after writeBusy readsState after callbackLater reads
legal FF → 50array read, program busy, 42-clock deadline80, C0array read, idle50
illegal 50 → D0error, program busy, 42-clock deadline00, 40error, idle20, 60 repeatedly

The illegal request stores 0x50. Program-busy status takes priority over the error state until the callback runs. The persistent error reads then set DQ5 and toggle DQ6. A following F0 write returns to array mode and reads 0x50. This differs from Wabbitemu’s one-read error lifetime. [confirmed]

The sector case seeds all 65,536 bytes at physical 0x200000x2FFFF to zero. The command changes all of them to 0xFF immediately and changes no byte outside that range. Erase-window reads are 00, 44; erase-busy reads after the first callback are 08, 4C; the second callback exposes array byte 0xFF. [confirmed]

Chip-erase overrideChanged bytesBytes left non-FFLast program address
group 0966,65681,9200xFA000
group 11,048,57600xFC000

Group 0 leaves 0xB00000xBFFFF and 0xFC0000xFFFFF unchanged. Both runs finish in array state with one 300-clock erase-window deadline for the last admitted sector. The native matrix also confirms the partial fast-program flow and its 90 F0 exit. Autoselect logs an unimplemented-command warning; CFI query does nothing; B0 in the erase-command window logs an undefined command and returns to array state without changing memory. [confirmed]

The native binary SHA-256 is 31f8e15a348d15f876f103b8452340484893987e458023fd913280365db5c51d. The build requires clean TilEm commit f56ad637d0524ee841dd381be6ecbaf5b8975600 and Git tree 58316afe35d69e69353f0f743698144153051d4a. These results describe the pinned emulator core, not the retail ROM worker or physical Flash. Build and run commands are under “Flash command and status matrix” in the repository’s tools/dynamic-tracing.md. [confirmed]

TilEm’s full calculator reset clears the Flash unlock gate, command state, and busy flag. It retains the last program address and byte, toggle state, protection-override group, and delay-emulation flags. An execution-protection exception reaches this reset only after the forbidden opcode completes. A guarded direct-core fixture executes LD (0x8000),A from restricted Flash page 08; its RAM write of 0x5A survives the reset. This ordering is TilEm behavior, not evidence that the ASIC executes a denied instruction. [standard] for source; [confirmed] for the pinned run. See TilEm reset and exception scope.

Wabbitemu behavior and limits

Wabbitemu recognizes byte program, sector erase, chip erase, autoselect, and fast-program commands. It applies program data with stored &= requested. Successful programming returns to array mode immediately. [standard]

An illegal 0→1 request sets an error flag. The next read returns complemented DQ7, set DQ5, and its current DQ6 toggle bit. That same read clears the error flag, so later reads return array data. This one-read lifetime is Wabbitemu behavior, not the hardware data-sheet polling contract. The ROM worker tests DQ7 and DQ5 in that same first byte. [standard] for Wabbitemu source; [confirmed] for the ROM worker.

Wabbitemu’s CPU_reset does not reset the Flash command step, error flag, toggle bit, write byte, delay, lock, or bounds. Its opcode-fetch path separately ends most non-read command states after an execution violation. A seeded FLASH_PROGRAM violation therefore returns to array mode before executing one boot instruction. A seeded FLASH_ERROR violation retains that command step; the boot instruction’s immediate-byte read consumes status 0xE0 and clears only the error flag. Both cases finish the same CPU_step at PC=0x0002. [standard] for the source paths; [confirmed] for the guarded native run. See Wabbitemu reset scope.

A guarded native run exercises seven byte pairs through Wabbitemu’s CPU_mem_write and CPU_mem_read entry points. Each case issues AA 55 A0, programs page 08 offset 0x0100, and reads the target twice. The harness unlocks the in-memory ASIC gate directly and replaces the target’s initial byte before the command. It does not execute the retail ROM worker. [confirmed] for the pinned Wabbitemu run.

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

The first three requests are legal and expose array data immediately. The four illegal requests set the error flag after programming initial & requested. Their first read clears that flag and flips DQ6. Their second read exposes the stored byte. All seven cases return to FLASH_READ; the initialized adapter adds zero T-states for these accesses, so this run provides no timing evidence. [confirmed] for the pinned Wabbitemu run.

The native binary SHA-256 is 67077107b604e97cfb751cadf4392dca53d00d5bbc417b2f48c422eebb9ac560. It uses pinned commit 48c2dc0e6d1d87bb5cf9611efbeb0d048b19c422 and the exact OS 2.55MP image. The guarded CLI checks every native field against the fixed launch expectations and the independent Python source model before writing its manifest.

The pinned Wabbitemu source and the ROM worker produce three paths. The first ROM read consumes Wabbitemu’s error status. The worker tests both DQ7 and DQ5 in that byte. Since Wabbitemu sets DQ5, every illegal request proceeds directly to one final array read. This table models emulator source combined with the byte-confirmed ROM poll logic. It does not describe physical Flash behavior. [standard] for Wabbitemu; [confirmed] for the ROM poll logic.

Program requestFinal stored DQ7ROM result
legalmatches requested DQ7succeeds on the first array read
illegal 0→1 outside DQ7matches requested DQ7succeeds after the final read even though lower requested bits remain zero
illegal DQ7 0→1differs from requested DQ7fails after the final read

Exhaustive enumeration of all 65,536 old/requested byte pairs gives 49,152 successes and 16,384 failures. No pair is nonterminating under this composition. The successes contain 6,561 legal pairs and 42,591 illegal requests that the ROM reports as successful. These exhaustive counts are deterministic consequences of the pinned source model, not an exhaustive Wabbitemu run or hardware observation. [standard]

A second guarded mode boots the exact retail ROM, injects a four-byte rst 28h/8087h harness into RAM page 1, and sets the documented _WriteFlashUnsafe ABI registers. The bcall copies the 124 bytes beginning at flash_program_worker_code to ramCode and executes them. The harness directly opens Wabbitemu’s in-memory ASIC gate, so it does not test the protected port-0x14 unlock sequence or an OS/UI caller. [confirmed] for the pinned native run.

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

All five cases enter the copied worker once and issue one program write. The legal request takes the success reset at ram:816B. The illegal lower-bit request also takes that path after its final DQ7 read, despite leaving bit 0 clear. Both illegal DQ7 requests take the failure reset at ram:8175, regardless of stored DQ5. DQ6 changes the first status byte but not the return path. [confirmed] for the pinned native run.

The cold-recovery runner exercises a separate retail path without opening the gate through emulator state. Startup at 00:0D73 calls the bjump stub at 00:3EEB, which resolves to 3D:6098. The bytes at 3D:609C3D:60A8 write 1 to port 0x14; Wabbitemu changes flash_locked from true to false at the OUT at 3D:60A6. The wrapper calls 00:2BAD at 3D:6101 to enter gc_check_interrupted at 3C:7BC7. Its return path jumps to the lock sequence at 3D:5CE6, and the OUT at 3D:5CEF changes flash_locked from false to true. The static gate scanner classifies both sequences and finds no unclassified port-0x14 candidate on page 3D. [confirmed]

All six reconstructed recovery images take that protected unlock → recovery → relock path. The observer identifies the public block-program worker by comparing all 124 bytes at ramCode with flash_program_worker_code. Each _WriteFlashUnsafe visit reaches one matching worker entry and one success tail at ram:816B; no run reaches the failure tail at ram:8175. [confirmed]

Input phase_WriteFlashUnsafe / worker entriesData writes at ramCode + 0x49 (ram:8149)_EraseFlash entries
0xFF33483
0xFE32473
0xFC20204
0xF819193
0xF030465,5603
0xE017172

These counts cover the exact public block-program worker. Other internal RAM workers can issue additional Flash commands during certificate rebuilding. The run is Wabbitemu evidence for the retail control path, not physical ASIC or Flash evidence. The observer binary SHA-256 is 242ca0d3ecab861ce1048285258d1e13ebc18a175bccf016397692fbe0f150db. It uses pinned Wabbitemu commit 48c2dc0e6d1d87bb5cf9611efbeb0d048b19c422. [confirmed]

Sector erase changes the complete sector to 0xFF before the next instruction and exposes no erase-busy interval. Its sector arithmetic matches the physical 64, 32, 8, 8, and 16 KiB top-boot layout. The two 8 KiB sectors are the halves of page 3E. [standard]

Wabbitemu also implements chip erase by filling the complete Flash array with 0xFF, including page 3F, without consulting its per-write boot-page gates. Its TI-84 Plus profile sets Flash version 3, which enables AA 55 20, repeated A0 program operations, and 90 F0 exit. It has no erase-suspend or CFI state. Autoselect offset 4 always returns zero, so it reports every sector as unprotected. [standard]

A guarded native command-family run checks these source claims through CPU_mem_write and CPU_mem_read. The adapter loads the exact OS 2.55MP image, opens Wabbitemu’s in-memory gate, and keeps every mutation in the allocated Flash array. It does not execute a retail-ROM Flash routine. [confirmed] for the pinned Wabbitemu run.

Command pathNative observation
AutoselectAA 55 90 enters FLASH_AUTOSELECT; offsets 0, 2, and 4 return 01, DA, and 00
Array resetF0 returns autoselect and a partial AA sequence to FLASH_READ
Fast programAA 55 20 enters FLASH_FASTMODE; two A0 operations store F0 & 50 = 50 and AA & A0 = A0, returning to fast mode after each
Fast-mode exit90 enters FLASH_FASTMODE_EXIT; F0 returns to FLASH_READ
Sector eraseAA 55 80 AA 55 30 changes all 65,536 seeded bytes at physical 0x200000x2FFFF to FF; no byte outside the range changes
Chip eraseAA 55 80 AA 55 10 reduces 322,043 non-FF bytes to zero and changes a seeded byte at physical 0xFFFFF from 00 to FF
CFI query98 from array mode returns to FLASH_READ and changes no byte
Erase suspend/resumeB0 in FLASH_ERASE_55 returns to FLASH_READ; the following 30 also leaves the array unchanged

The sector test seeds the complete 64 KiB sector and two adjacent boundary bytes before issuing the command. The chip test counts the complete 1 MiB array and explicitly seeds its final boot-page byte. The adapter records zero T-states for the direct calls, so the run provides no command-timing evidence. Its binary SHA-256 is 41304b9a760438440f60cbfeca394cd37252c929ef2043e692c0254b8d1cb52d. [confirmed] for the pinned Wabbitemu run.

Wabbitemu accepts a port-0x14 write only while the current Flash page passes its privileged-page predicate. Its source names pages 2F, 3C, 3D, and 3F for the TI-84 Plus path; page 3E does not pass that predicate. A separate write-validity check requires the resulting unlocked state and applies model flags to boot-page writes. This approximates the ASIC gates but does not model TilEm’s byte-fetch recognizer. [standard]

MAME behavior and limits

MAME 0.287 instantiates its generic AMD_29F800T device for the TI-84 Plus. The device has a one-megabyte array, AMD manufacturer ID 0x01, device ID 0xDA, and top-boot sector geometry. [standard]

Its AMD autoselect path returns the configured maker ID at offset 0, device ID at offset 1, and a fixed zero at offset 2. It does not implement the Fujitsu byte-mode offsets 0, 2, and 4, including the sector-protection read at offset 4. It has no CFI query state for AMD_29F800T and no AMD erase-suspend state. The 0xB0 case in this source is a Sanyo-specific bank-select command, not erase suspend. [standard]

Its 8-bit byte-program path assigns stored = requested. It does not apply NOR AND semantics. A request to change a stored zero to one therefore succeeds in MAME, and the first ROM poll reads the assigned byte with matching DQ7. The program path has no timed busy mode or DQ5 failure state. [standard]

Sector erase fills the selected sector with 0xFF immediately, then enters a timed status mode. MAME uses 1,000 ms for a 64 KiB sector, 500 ms for the 32 KiB and 16 KiB sectors, and 250 ms for either 8 KiB sector. In-sector reads alternate 0x4C and 0x08, toggling DQ6 and DQ2 around a base DQ3 value. DQ5 stays clear. When the timer expires, reads return the already-erased array. [standard]

The busy-read range has a separate geometry bug. MAME tests a 64 KiB interval from m_erase_sector even when it erased a 32, 16, or 8 KiB top-boot sector. Erasing the first page-3E half at 0xF8000, for example, returns erase status for every read through 0xFFFFF until the 250 ms timer ends. Only the selected 8 KiB array region is changed. [standard]

Chip erase fills the complete array with 0xFF immediately and starts the generic 16-second AMD_29F800T erase timer. The chip-erase branch does not set m_erase_sector. Busy reads consequently use its stale or initial value and a 64 KiB interval even though the complete array has already changed. [standard]

MAME accepts AA 55 20 and sets its fast-mode flag. Its subsequent 0xA0 handler permits fast programming only for Fujitsu and selected ST maker IDs. The 0x90 fast-exit transition uses the same maker-ID gate. The TI-84 Plus instance uses maker ID 0x01 for AMD, so A0 logs an unknown mode byte and 90 F0 returns to normal reads without clearing the fast-mode flag. MAME therefore has partial, not working, unlock-bypass support for this device configuration. [standard]

The TI driver maps every Flash bank directly to the generic device’s read and write methods. Port 0x14 stores m_flash_unlocked and updates paging, but no memory-write path consults that value. The driver also omits ports 0x220x28. MAME therefore accepts command writes without the protected byte sequence, sector override, or execution-protection state used by the ROM. [standard]

The stored gate is a raw byte rather than a Boolean. A guarded sweep of writes 00 01 02 3F 40 FF makes port 0x02 return C3 C7 CB FF C3 FF, following the driver’s truncated 0xC3 | (value << 2) expression. Port 0x14 itself reads zero. A scheduled soft reset retains write one and consequently returns 0xC7; this is MAME reset behavior, not a physical lock-retention result. [standard]

A separate guarded run maps Flash page 08 into the CPU’s 0x4000 window and issues commands through CPU program space while reading the gate state through I/O port 0x02. A complete program while locked reports C3 and changes the target from FF to 50. A prefix started while locked and completed after an unlock reports C7 and changes it to D0. A prefix started while unlocked and completed after relocking reports C3 and changes it to 20. CPU reads and direct generic-device reads agree after every case. [confirmed]

The saved image differs from the source only at 0x20100 (FF → 20) and has SHA-256 2fd21a6b139a641d40a71a0e68df492e4555e79c6f1cf44858b4dcfd9158bbeb. This CPU/I/O-space result confirms that MAME stores and exposes the port-0x14 state without applying it to mapped Flash writes. It describes MAME 0.287, not the ASIC gate or physical Flash. [confirmed]

A guarded MAME 0.287 run exercises the ti84pv3 machine’s mapped :membank0 Flash interface through Lua. It uses the exact OS 2.55MP image but does not execute TI-OS Flash code. The report oracle checks every field against the pinned source model, and the image oracle compares the complete saved 1 MiB array against the expected command mutations. [confirmed]

Command or readRuntime observation
Autoselectoffsets 0, 1, 2, and 4 return 01, DA, 00, and 00
Byte programFF → 50 stores 50; the illegal 50 → D0 request stores D0
Array reset and CFIF0 after a partial unlock restores array reads; 98 leaves the programmed D0 visible
Unlock bypassAA 55 20 accepts the entry, but its A0 program does not change D0; 90 exposes manufacturer ID 01, and F0 restores array byte D0
8 KiB top-sector erasethe selected 0xF80000xF9FFF array range changes immediately; reads at 0xF8000, 0xFA000, and 0xFC000 expose busy status, while 0xE0000 remains an array read
Timer completionselected and adjacent reads return FF, boot Flash returns 3E, and 0xE0000 returns 9F at frame 20

The saved Flash differs from the source ROM only at 0x20100 (FF → D0), 0xF8000 (00 → FF), and 0xF9FE00xF9FE1 (00 → FF). Its SHA-256 is 1dc4eec678252588df24118e96603b6c80806b8b9ea8e0e12b2169ac6aae3935. The MAME executable SHA-256 is fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91. These identities and the retained manifest scope the result to MAME 0.287, not the retail worker or physical Flash. [confirmed]

A separate guarded run seeds each sector boundary and its adjacent probes with 00 before issuing five sector erases. Each command changes only its selected array range. Busy reads cover 64 KiB from the selected start, including bytes past the 32 KiB and 8 KiB sectors. [confirmed]

Selected rangeSource timerCompletion frameOut-of-sector read while busy
0xE00000xEFFFF1,000 ms500xF0000 = 00
0xF00000xF7FFF500 ms750xF8000 = 08
0xF80000xF9FFF250 ms880xFA000 = 08
0xFA0000xFBFFF250 ms1010xFC000 = 08
0xFC0000xFFFFF500 ms1260xFBFFE = 00

The frame deltas are 50, 25, 13, 13, and 25. At 50 frames/s, they match the source timers. The 250 ms cases appear on the next whole frame. Every seeded byte immediately outside the selected array range remains 00 after completion. [confirmed]

Chip erase starts at emulated second 2 after the sector matrix. It immediately changes the complete array to FF, while reads in the last sector’s stale busy range return 4C and 08. The periodic state probe observes array reads at second 18, exactly 16 emulated seconds later. The saved image contains no non-FF byte and has SHA-256 f5fb04aa5b882706b9309e885f19477261336ef76a150c3b4d3489dfac3953ec. [confirmed]

Reproducing the comparison

tools/flash_hardware.py contains the photographed-device specification, reported compatible families, sector table, source-modeled program rules, MAME erase status, and the ROM worker’s DQ7/DQ5 decision. The focused CLI separates physical and emulator identities and exposes negative cases without modifying an emulator:

$ python tools/describe_flash_hardware.py parts
photographed part: Fujitsu MBM29LV800TA-70PFTN
  package marking: 29LV800TA-70PFTN
  board evidence: Datamath March 2004 TI-84 Plus PCB photograph
  data-sheet autoselect: manufacturer=0x04 device=0xDA
  rated byte program: 8 us typical, 300 us maximum
  rated sector erase: 1 s typical, 10 s maximum
reported compatible families: AMIC A29L800A, Fujitsu 29LV800, Spansion S29AL008D, Macronix MX29LV800
$ python tools/describe_flash_hardware.py program --old 0x00 --data 0xFF
program old=0x00 requested=0xFF
  TilEm: stored=0x00 poll=error state
  Wabbitemu: stored=0x00 poll=one transient error-status read
  MAME: stored=0xFF poll=array data
  Wabbitemu error-read values (DQ6=0/1): 0x20 0x60

The Wabbitemu/ROM composition is available for one pair or as an exhaustive summary. The single-pair model defaults the persistent DQ6 toggle bit to clear; --dq6 selects a set bit. DQ6 changes the first read value but not the ROM’s DQ7/DQ5 decision.

$ python tools/describe_flash_hardware.py wabbitemu-poll --old 0x50 --data 0xD0
Wabbitemu/ROM old=0x50 requested=0xD0 stored=0x50
  read 0: DQ7/DQ5 poll=0x20 -> need-final-read
  read 1: final DQ7 poll=0x50 -> failure
  outcome: failure
$ python tools/describe_flash_hardware.py wabbitemu-poll
all byte pairs: 65536
  outcomes: success=49152 failure=16384
  legal successes: 6561
  illegal requests reported successful: 42591
$ python tools/describe_flash_hardware.py mame-erase 0xF9000 --reads 4
sector 0x0F8000-0x0F9FFF, timer=250 ms
busy reads 0x0F8000-0x0FFFFF
status: 0x4C 0x08 0x4C 0x08

The command-capability matrix and structural ROM scan are available as JSON:

python tools/describe_flash_hardware.py --json commands
nix develop -c python tools/analyze_flash_rom_commands.py --json

The guarded MAME runtime probe requires the exact MAME binary hash and writes its command, input identities, report, complete NVRAM image comparison, and captured logs to a new output directory:

mame_flash_parent=$(mktemp -d /tmp/ti84-mame-flash.XXXXXX)
nix shell nixpkgs#mame --command python tools/run_mame_flash_probe.py \
  --expected-mame-sha256 \
    fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
  --output-dir "$mame_flash_parent/run" --json

The CPU-visible gate probe uses the same guarded runtime and changes the gate between AMD command phases:

mame_gate_parent=$(mktemp -d /tmp/ti84-mame-gate.XXXXXX)
nix shell nixpkgs#mame --command python tools/run_mame_flash_gate_probe.py \
  --expected-mame-sha256 \
    fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
  --output-dir "$mame_gate_parent/run" --json

The independent erase matrix uses the same guards and output contract:

mame_erase_parent=$(mktemp -d /tmp/ti84-mame-erase.XXXXXX)
nix shell nixpkgs#mame --command python tools/run_mame_flash_erase_probe.py \
  --expected-mame-sha256 \
    fc5f4aba1aa6eb115d66decad13bb3f5313b9f3be9cff7c785d8d88e3fca0b91 \
  --output-dir "$mame_erase_parent/run" --json

The parts, geometry, profiles, commands, poll, and wabbitemu-poll subcommands support --json for scripts. tools/flash_trace.py imports the same geometry library, so dynamic trace reports and emulator comparisons use one sector definition.

Quirks and unresolved hardware questions

  • Page-guard rejection returns Z, while an accepted-page zero-length call returns NZ. The read-only TilEm fixture captures all three cases without a Flash write. Callers cannot interpret Z as proof that programming occurred. [confirmed]
  • A locked _WriteAByte request can return Z under TilEm without changing the target when the requested and stored DQ7 bits already agree. Port 0x02 and the final array read confirm that the gate remained locked and the target remained 0x50. Physical ASIC behavior remains unmeasured. [confirmed] for the ROM and TilEm trace; [hypothesis] for hardware.
  • The internal page-3D certificate programmer returns the saved port-0x06 page in A after a DQ5 failure. Saved page zero therefore produces Z, and its only direct caller ignores the flags in every case. A guarded TilEm fixture reproduces the Z failure with an unchanged target. Physical DQ5 behavior remains unmeasured. [confirmed] for the ROM and TilEm trace; [hypothesis] for hardware.
  • _EraseFlashPage also rejects page 3E with Z. The certificate-sector wrapper restores caller AF after valid and invalid inputs, so it does not expose an erase result through flags. A guarded TilEm erase confirms this with a successful worker and an unchanged caller AF. [confirmed]
  • _WriteFlash’s page-3E crossing behavior is byte-confirmed and dynamically reproduced in TilEm with an emulator-only patched-ROM fixture. It remains untested on a physical calculator. [confirmed] for the ROM and emulator trace; [hypothesis] for physical consequences.
  • _EraseFlash’s failure path uses undocumented DE as a reset-command pointer. Two internal certificate paths leave a Flash address there, while the 3D:60EE reset path leaves inherited DE, the 3D:71C3 path carries metadata, and the public bcall accepts arbitrary DE. A forced physical DQ5 test with DE in RAM is still required. [confirmed] for the ROM paths; [hypothesis] for physical failure behavior.
  • The precise physical ASIC implementation of the protected-byte recognizer is represented here by WikiTI and TilEm behavior. The calculator schematic does not expose the ASIC’s internal state machine. [standard]
  • Physical tests still need to measure legal and illegal byte-program status reads, including a requested 0→1 transition. Guarded native matrices pin the differing TilEm, Wabbitemu, and MAME results. None establishes physical behavior. [confirmed] for the pinned emulator runs; [hypothesis] for hardware.
  • The Fujitsu data sheet bounds byte program at 300 µs and sector erase at 10 s, with 8 µs and 1 s typical values. Calculator-level duration, DQ toggle cadence, erase-suspend behavior, and top-boot busy-read boundaries remain unmeasured. [standard] for the part limits; [hypothesis] for behavior on a particular calculator.
  • Physical tests have not exercised chip erase, autoselect sector-protection reads, fast programming, or erase suspend/resume. Emulator agreement cannot fill those gaps because the pinned implementations disagree with the Fujitsu command table or omit the states. [hypothesis]
  • The collector’s normal sector-copy policy and persistent phase dispatcher are reconstructed. TilEm cold-restart traces exercise all six ROM-written journal phases. Active 0xFF, 0xFE, 0xFC, 0xF8, and 0xE0 converge byte-for-byte with uninterrupted execution. Active 0xF0 has matching archive bytes and converges after the uninterrupted result performs deferred 0xE0 cleanup on its next boot. A deterministic eight-record constructor reproduces the record-authentic 0xF0 input byte for byte. Pinned Wabbitemu independently executes all six dispatcher branches and produces the corresponding complete TilEm images. Cuts during busy commands and physical power loss remain untested. [confirmed] for the emulator command-boundary runs; [hypothesis] for the remaining cases.
  • Pinned Wabbitemu cold recovery takes the retail startup path from 00:0D73 through the protected unlock at 3D:60A6, gc_check_interrupted at 3C:7BC7, public Flash bcalls and copied block workers, and the relock at 3D:5CEF. All six phase images take this path. [confirmed] for Wabbitemu; [hypothesis] for physical gate behavior.
  • A controlled _ReceiveOS_USB run shows that _DisplayOSProgress precedes validation of an installer record’s page byte. Seeding the already-displayed page to 0x3E immediately before that helper isolates the downstream page validator: page 0x3E reaches 2F:49A2, runs _USBErrorCleanup, and leaves the complete Flash array unchanged. This intervention does not establish the natural progress-byte behavior of a complete OS-install session. [confirmed] for the isolated Wabbitemu-core run; [hypothesis] for physical behavior.

Sources

SourceUse
WikiTI certificate headersliterature labels for certificate-tail offsets, kept separate from ROM-derived ownership
Wabbitemu 83psehw.c at 48c2dc0independent port-0x02 family-bit implementation
WikiTI port 0x14Flash command lock and certificate read protection
WikiTI protected portsprivileged pages and protected-byte sequence
WikiTI _WriteFlash and _WriteFlashUnsafepublic ABI and RAM-source requirement
WikiTI _EraseFlashsector-erase ABI and granularity warning
WikiTI ports 0x21, 0x22, and 0x23chip selection and Flash execution limits
Datamath TI-84 Plus hardware and March 2004 PCB photographFujitsu vendor identification and photographed 29LV800TA-70PFTN marking
Datamath memory-component indexreported AMIC, Fujitsu, Spansion, and Macronix compatible families
Fujitsu MBM29LV800TA/BA data sheet, DS05-20845-4Eexact part organization, suffixes, command table, autoselect IDs, status bits, polling algorithm, timing, and endurance; audited 59-page PDF SHA-256 552a0ebc1de06b64507b7226e1d5bf4cebf8f61d6b5820e0cc796b1985186b19; former DatasheetArchive download URL returned 404 on 2026-08-09
TilEm flash.c, calcs.c, z80.c, x4_memory.c, x4_io.c, and x4_subcore.cpinned commit f56ad637d0524ee841dd381be6ecbaf5b8975600; flash.c SHA-256 280e0e45b6e1f1ef21d779abb809eaef2d04d08db09feb87a459e079280c9545; emulator command state, ASIC gates, sector table, full reset, and exception ordering
Wabbitemu core.c, core.h, and 83psehw.cpinned commit 48c2dc0e6d1d87bb5cf9611efbeb0d048b19c422; file SHA-256 values 7e7552577b9934a8e344d0bea8152e2b46ddf6840e997e478723cfde7c170c2b, 6add613d150b55ffdabc8a784e1261b1fcac6e27f0519b1da835de4064b790ec, and 3acba050bde4df46348aac703899e2980efb24b5fec83f3f0b5940a47f8327c4; command state machine, erase geometry, and ASIC gates
MAME intelfsh.cpp, intelfsh.h, ti85.cpp, and ti85_m.cpppinned tag mame0287; file SHA-256 values 8fb7e74656801c7939246c9bc77dceab3b36561df33d9ef4201f786eb6713da0, 42837497b8d3dfdcf1f1119168ae87bf4583c19238acf078c0efcf5dca1e64f9, 33d77ae3ffc373088202cf79d9979d2a9b715eb1f451122cfd764d1a911d75a1, and ae9f8986a80a4ea3ee00c801787f48edb0447880099612949c3429017d1cdedf; generic AMD device behavior and TI-84 Plus mapping
jsTIfied project 42, deployed 20170706a artifact, and readable mirror at 56246a1deployed artifact SHA-256 c7325a38f976f64eaa34182da17d838fe4831eece4650b92d5db710cf7a8fc5b; fourth emulator implementation of geometry, commands, protection, and immediate mutation. The mirror aids review but is not byte-identical to the deployed artifact.