Skip to main content
The Emu struct is the main emulator orchestrator that coordinates the CPU, bus, and peripherals to run the TI-84 Plus CE. It provides the high-level API for running the emulator and managing state.

Overview

The Emu struct is defined in core/src/emu.rs and contains:
pub struct Emu {
    cpu: Cpu,                  // eZ80 CPU
    bus: Bus,                  // System bus (memory, I/O)
    scheduler: Scheduler,      // Event scheduler
    framebuffer: Vec<u32>,     // ARGB8888 framebuffer
    rom_loaded: bool,          // ROM loaded flag
    powered_on: bool,          // Calculator powered on
    history: ExecutionHistory, // PC/opcode history for diagnostics
    total_cycles: u64,         // Total cycles executed
    // ... internal state
}

Creating an Emulator

Rust API

use ti84ce_core::Emu;

let mut emu = Emu::new();

C API

#include "emu.h"

void* emu = emu_create();
// ... use emulator
emu_destroy(emu);
The C API uses SyncEmu, a thread-safe wrapper that contains a Mutex<Emu>. All FFI operations are synchronized to prevent data races.

Loading ROM

Before running the emulator, you must load a TI-84 Plus CE ROM:
use std::fs;

let rom_data = fs::read("TI-84 CE.rom")?;
emu.load_rom(&rom_data)?;

ROM Requirements

  • Size: Up to 4MB (0x400000 bytes)
  • Format: Raw binary dump from TI-84 Plus CE
  • Source: Extracted from calculator or official TI OS
The ROM is copyrighted by Texas Instruments and cannot be redistributed. Users must extract their own ROM from their calculator.

Powering On

After loading the ROM, power on the calculator:
emu.power_on();
This simulates pressing the ON key. The CPU begins executing at PC=0x000000.

Running Cycles

The main emulation loop runs a specified number of CPU cycles:
// Run 1 frame worth of cycles (48MHz / 60fps = 800000 cycles)
let executed = emu.run_cycles(800_000);
println!("Executed {} cycles", executed);

Cycle Counting

The emulator returns the actual number of cycles executed, which may differ from the requested amount due to:
  • HALT instruction: CPU stops until interrupt or key press
  • Device power off: User pressed 2nd+ON or OS triggered APD
  • Breakpoint hit: Debug breakpoint reached (Rust API only)

Frame Timing

For 60 FPS emulation at 48 MHz:
const FRAME_CYCLES: u32 = 48_000_000 / 60; // 800,000 cycles/frame

loop {
    let start = Instant::now();
    emu.run_cycles(FRAME_CYCLES);
    emu.render_frame(); // Update framebuffer
    
    let elapsed = start.elapsed();
    if elapsed < Duration::from_millis(16) {
        thread::sleep(Duration::from_millis(16) - elapsed);
    }
}

Framebuffer Access

The emulator maintains a 320×240 ARGB8888 framebuffer:
emu.render_frame(); // Update framebuffer from VRAM
let (width, height) = emu.framebuffer_size(); // (320, 240)
let pixels = emu.framebuffer(); // &[u32]

// Each pixel is 0xAARRGGBB
for y in 0..height {
    for x in 0..width {
        let pixel = pixels[y * width + x];
        let r = ((pixel >> 16) & 0xFF) as u8;
        let g = ((pixel >> 8) & 0xFF) as u8;
        let b = (pixel & 0xFF) as u8;
        // Render pixel...
    }
}
The framebuffer is updated by calling render_frame() (Rust) or automatically during emu_run_cycles() (C API).

Key Input

The keypad is an 8×8 matrix. Press/release keys with:
// Press ENTER key (row 6, col 1)
emu.set_key(6, 1, true);

// Release ENTER key
emu.set_key(6, 1, false);

Key Matrix Layout

The 8×8 key matrix maps to physical keys:
RowCol 0Col 1Col 2Col 3Col 4Col 5Col 6Col 7
0GraphTraceZoomWindY=2ndModeDel
1StoLnLogx⁻¹MathAlpha
20147,SinAppsX,T,θ
3.258(CosPrgmStat
4(-)369)TanVars
5Enter+-×÷^Clear
6DownLeftRightUp
7
Row/column indices are zero-based. For example, the ENTER key is at row 5, column 0 (not row 6, column 1 as shown in some diagrams).

State Management

Save State

let size = emu.save_state_size();
let mut buffer = vec![0u8; size];
emu.save_state(&mut buffer)?;

std::fs::write("state.sav", &buffer)?;

Load State

let data = std::fs::read("state.sav")?;
emu.load_state(&data)?;

Sending Files

You can send .8xp (programs) and .8xv (variables) files to the emulator:
let file_data = std::fs::read("program.8xp")?;
let count = emu.send_file(&file_data)?;
println!("Injected {} entries", count);
Files must be sent after load_rom() but before power_on(). The emulator injects them into the flash archive so TI-OS discovers them on boot.

LCD State

Query LCD state for accurate display rendering:
let backlight = emu.get_backlight(); // 0-255
let lcd_on = emu.is_lcd_on();

if lcd_on {
    // Render framebuffer with backlight brightness
} else {
    // Display is off, show black screen
}

Public Methods

Core Methods

impl Emu {
    pub fn new() -> Self;
    pub fn reset(&mut self);
    pub fn load_rom(&mut self, data: &[u8]) -> Result<(), i32>;
    pub fn power_on(&mut self);
    pub fn run_cycles(&mut self, cycles: u32) -> u32;
}

Display Methods

impl Emu {
    pub fn render_frame(&mut self);
    pub fn framebuffer(&self) -> &[u32];
    pub fn framebuffer_size(&self) -> (usize, usize);
    pub fn get_backlight(&self) -> u8;
    pub fn is_lcd_on(&self) -> bool;
}

Input Methods

impl Emu {
    pub fn set_key(&mut self, row: usize, col: usize, down: bool);
}

State Methods

impl Emu {
    pub fn save_state_size(&self) -> usize;
    pub fn save_state(&self, buffer: &mut [u8]) -> Result<usize, i32>;
    pub fn load_state(&mut self, data: &[u8]) -> Result<(), i32>;
}

File Transfer Methods

impl Emu {
    pub fn send_file(&mut self, file_data: &[u8]) -> Result<usize, i32>;
    pub fn send_file_live(&mut self, file_data: &[u8]) -> Result<usize, i32>;
}

Internal Architecture

The Emu struct orchestrates several subsystems:

Execution Flow

  1. Fetch: CPU reads instruction from memory via bus
  2. Decode: CPU decodes opcode and determines operation
  3. Execute: CPU performs operation, updating registers/memory
  4. Tick: Peripherals advance by elapsed cycles
  5. Schedule: Check for pending events (LCD refresh, timer overflow, etc.)
  6. Interrupt: Check for pending interrupts and handle if enabled

Next Steps

Memory Types

Learn about Flash, RAM, and memory map

CPU Module

Explore the eZ80 CPU implementation

Peripherals

Understand peripheral controllers

Testing

Test the emulator with boot/trace tools

Build docs developers (and LLMs) love