How to use the `Exec` command on the TI-92 Plus and Voyage 200
Warning: Calling exec() on your calculator might erase all data! Make sure to have a backup on your PC if you have anything important on it!
Why I care
When I got my TI-92 calculator back in 1998, I started writing games in the built-in Program Editor. But it always bothered me, that I couldn’t access the whole screen.

It was possible to draw arbitrary pixels to anything but the red areas. The top area could be changed to a custom toolbar, but that was it.
Sadly, this didn’t change when I got the next version of that calculator, the TI-92 Plus, two years later. But, there were games on the Internet that were written in Assembly that used the whole screen. So it was possible, somehow!
Since I learned everything I knew about the calculator from the excellent documentation in the TI-92 Guide, I turned to the successor, the TI-92 Plus Guidebook. I didn’t find any command that made it possible. But there was one new command called Exec, that sounded promising. Here’s the documentation for it:
Exec string [, expression1] [, expression2] …
Executes a string consisting of a series of Motorola 68000 opcodes. These codes act as a form of an assembly-language program. If needed, the optional expressions let you pass one or more arguments to the program.
For more information, check the TI Web site: education.ti.com
Warning: Exec gives you access to the full power of the microprocessor. Please be aware that you can easily make a mistake that locks up the calculator and causes you to lose your data. We suggest you make a backup of the calculator contents before attempting to use the Exec command.
Visiting education.ti.com was of no help to me. I just couldn’t figure out how to use this command. Even if I knew what opcodes I wanted to execute, how would I convert them to a string?
Trial and error didn’t work either. In fact, calling exec("") would crash the calculator and erase all programs.
Now, about 25 years later, I stumbled upon a website that shows an example for a string that doesn’t crash the calculator: exec("4e750000"). And what can I say? This command does nothing, but made me so, so happy!
Finally a working example of the Exec command!
But how do you go from not crashing to drawing pixels on the screen?
Accessing the whole screen
We now know that exec() expects a hex-string as its first argument, which represents the Motorola 68000 opcodes. Great! But what opcodes draw pixels on the screen?
The TI-89 / TI-92 Plus Developer Guide mentions an LCD buffer. That sounds promising! It lives on the address range from 0x004C00 to 0x005AFF.
But 0x005AFF to 0x004C00 is a range of only 3840 addresses. The display of the calculator has a resolution of 240x128 = 30720 pixels. My guess is that each address contains 8 pixel values (a byte), because 3840 addresses times 8 equals 30720 possible pixels.
How can we test this?
Let’s try to set every address to 0. It should either clear the whole screen, or make it black. Depending on how the calculator interprets it.
Next step: What are the opcodes to set a byte to 0 on a specific address?
The Motorola M68000 Family Programmer’s Reference Manual documents some Assembly MOVE instructions. One of those should work. It also shows how those instructions can be translated into binary. Converting binary to hex is trivial, so let’s start!
Here’s what I want to do in pseudocode:
var A0 = 0x4C00; // A0 = start of the LCD-buffer
const A1 = 0x5B00; // A1 = end of the LCD-buffer + 1
LOOP:
setByte(address = A0, byte = 0);
A0 = A0 + 1;
const isEndOfBufferReached = (A0 == A1); // returns false until A0 reaches address A1
if (!isEndOfBufferReached) {
goto LOOP;
}
And here’s how that looks in assembly:
MOVEA.W #$4C00,A0 ; A0 = start of the LCD-buffer
MOVEA.W #$5B00,A1 ; A1 = end of the LCD-buffer + 1
LOOP: ; label called "LOOP" so we can jump here
MOVE.B #0,(A0) ; set a byte (= 8 bits) to "0"
ADDQ.L #1,A0 ; A0 = A0 + 1
CMPA.L A1,A0 ; compare A1 == A0
BNE.S LOOP ; if comparisson == false then go to "LOOP"
RTS ; tell the code that called our program to resume
Let’s start translating each line of assembly into hex-strings:
MOVEA.W #$4C00,A0
The opcode structure for MOVEA.W consists of 16 bits, bold values are constant:
| Bits | Value | Meaning |
|---|---|---|
| 15–14 | 00 | MOVE |
| 13–12 | 11 | Size (W) |
| 11–9 | 000 | Destination register = A0 |
| 8–6 | 001 | Destination mode = 001 → Address Register (An) |
| 5–3 | 111 | Source mode = 111 → Immediate |
| 2–0 | 100 | Source register = 100 → #<data> |
When concatenating the value-column we get 0011 0000 0111 1100 which is 307C in hex. Since the “source mode” is set to “immediate” (111) and the source register to ”#<data>” (100), we follow it with the value we want to put into register: 4C00, which is already in hex format (as signified by the $ in front of it.)
Therefore:
MOVEA.W #$4C00,A0 translates to 307C 4C00 in hex.
MOVEA.W #$5B00,A1
This line should be the same, except of the destination register and the #<data>.
So lets change the destination register to 001 to use register A1 and the #<data> to 5B00:
| Bits | Value | Meaning |
|---|---|---|
| 15–14 | 00 | MOVE |
| 13–12 | 11 | Size (W) |
| 11–9 | 001 | Destination register = A1 |
| 8–6 | 001 | Destination mode = 001 → Address Register (An) |
| 5–3 | 111 | Source mode = 111 → Immediate |
| 2–0 | 100 | Source register = 100 → #<data> |
0011 0010 0111 1100 is 327C in hex. Append 5B00 and we get:
MOVEA.W #$5B00,A1 translates to 327C 5B00 in hex.
LOOP (label)
Let’s forget about the LOOP-label for now. I think the compiler just uses labels to calculate how many bytes the final code should jump back in the BNE.S <label> instruction. It does not have any opcode that we need to translate.
MOVE.B #0,(A0)
The opcode structure for MOVE almost looks like MOVEA, but allows you to specify the Destination Mode, too:
| Bits | Value | Meaning |
|---|---|---|
| 15–14 | 00 | MOVE |
| 13–12 | 01 | Size (B) |
| 11–9 | 000 | Destination register = A0 |
| 8–6 | 010 | Destination mode = 010 → Address Register Indirect ((An)) |
| 5–3 | 111 | Source mode = 111 → Immediate |
| 2–0 | 100 | Source register = 100 → #<data> |
We now use the Address Register Indirect mode, because we want to write the byte 0000 0000 to the address stored in the register A0. We don’t want to write the zero-byte into the register itself, that would overwrite our address.
The value column gives us 0001 0000 1011 1100 which is 10BC in hex. Append 0000 and we get:
MOVE.B #0,(A0) translates to 10BC 0000 in hex.
ADDQ.L #1,A0
The opcode structure for ADDQ looks like this:
| Bits | Value | Meaning |
|---|---|---|
| 15–12 | 0101 | ADDQ |
| 11–9 | 001 | Data = 1 |
| 8 | 0 | |
| 7–6 | 10 | Size (L) |
| 5-3 | 001 | Effective Address Mode = 001 → Address Register (An) |
| 2-0 | 000 | Effective Address Register = A0 |
Add Quick (ADDQ) lets us encode values between 1 and 8 directly into the opcode (in the three data-bits): 0101 0010 1000 1000 is 5288 in hex, which leads us to:
ADDQ.L #1,A0 translates to 5288 in hex.
CMPA.L A1,A0
The opcode structure for CMPA looks like this:
| Bits | Value | Meaning |
|---|---|---|
| 15–12 | 1011 | CMPA |
| 11–9 | 000 | Register = A0 |
| 8-6 | 111 | Opmode = 111 → Long operation |
| 5-3 | 001 | Effective Address Mode = 001 → Address Register (An) |
| 2-0 | 001 | Effective Address Register = A1 |
CMPA subtracts the source (the “Effective Address” A1) from the destination (A0) and sets condition codes N,Z,V, and C. We need the condition code (cc) “Z”, which is set if the result was zero (= the values are identical).
The value column gives us 1011 0001 1100 1001 which is B1C9 in hex.
CMPA.L A1,A0 translates to B1C9 in hex.
BNE.S LOOP
The opcode structure for BNE looks like this:
| Bits | Value | Meaning |
|---|---|---|
| 15–12 | 0110 | Branch Conditionally (Bcc) |
| 11–8 | 0110 | Condition Code (cc) = 0110 → Not Equal (NE) |
| 7-0 | 1111 0110 | Displacement (How many bytes to jump as a signed 8-bit number) |
Lets understand the Displacement part real quick:
We have the following hex-code, that represents the code after our LOOP label, plus the branch-instruction we are currently building: 10BC 0000 5288 B1C9 66xx. So we want to tell the program to jump back these 10 bytes. (Two hex characters = one byte.)
That means we want to encode -10, using the Two’s complement: 256-10 = 246 or 1111 0110 in binary.
Therefore, the value column gets us 0110 0110 1111 0110 which is 66F6 in hex.
BNE.S LOOP (where LOOP is a label, signifying a 10-byte jump back) translates to 66F6 in hex.
RTS
The opcode for RTS is a constant, it has no changing parts.
RTS translates to 4E75 in hex.
Putting it all together: The hex-string
After all that work of manually compiling our assembly program we get the following hex-string: 307C 4C00 327C 5B00 10BC 0000 5288 B1C9 66F6 4E75.
Here’s the assembly code with the hex codes included:
MOVEA.W #$4C00,A0 ; 307C 4C00 A0 = start of the LCD-buffer
MOVEA.W #$5B00,A1 ; 327C 5B00 A1 = end of the LCD-buffer + 1
LOOP: ; label called "LOOP" so we can jump here
MOVE.B #0,(A0) ; 10BC 0000 set a byte (= 8 bits) to "0"
ADDQ.L #1,A0 ; 5288 A0 = A0 + 1
CMPA.L A1,A0 ; B1C9 compare A1 == A0
BNE.S LOOP ; 66F6 if comparison == false then go 10 bytes back
RTS ; 4E75 tell the code that called our program to resume
BUT WAIT!
We need to add 0000 to the end of that string, or else the calculator complains about “Invalid relocation data in ASM program”.
0000 terminates a relocation table. I’m not sure what a relocation table is exactly, but we need to tell the Exec command that it has ended. Always. Even if there’s none.
Now we are ready! Type the following into your TI-92 Plus or Voyage 200: exec("307c4c00327c5b0010bc00005288b1c966f64e750000"). It doesn’t matter if you use uppercase or lowercase letters, but remove any spaces that I added for readability!
You should see this:

Wait! Why is there still text on the screen? Shouldn’t it be all white?
It looks like we successfully clear the whole screen, but after returning (remember the RET-instruction?) the TI-92 Plus operating system takes over and redraws part of the Home-screen.
If you remove the RET-instruction (don’t!), the calculator will crash with an address-error. (But you will see, that you really zeroed all pixels.)
Nice! Now it’s your turn: Try to find out what part of the hex-string you need to replace with FF to set all pixels to black. Like this:

Bonus: Wait for keypress before exiting
The following code loops and exits when a key is pressed:
MOVE.B #$00,$600018 ; 13FC 0000 0060 0018 Set all keys to "not masked"
MOVE.B #$00,$600019 ; 13FC 0000 0060 0019 Set all keys to "not masked"
WAIT_FOR_KEY:
MOVE.B $60001B,D0 ; 1039 0060 001B Save keyboard column mask in D0
CMPI.B #$FF,D0 ; 0C00 00FF Is any bit 0? If yes, then one or more keys are held down
BEQ.S WAIT_FOR_KEY ; 67 F4 Jump 12 bytes back
The unofficial Hardware Guide documents the address 60001B as follows:
Keyboard column mask; if a bit is clear, one or more keys in the corresponding column are being held down. Keys in rows masked by [
600018] are ignored.
Note: 600019 is also part of the key mask, according to the source of the JavaScript emulator.
The code compiles to this hex string: 13fc00000060001813fc00000060001910390060001b0c0000ff67f4.
If you put this hex-string after our loop and just before RTS, you should see the blank (or black) screen until you press a key.
That is, if you release the Enter key fast enough before our code executes. If you don’t, the still pressed key will trigger our “wait for key”-code and end the program immediately.
Here’s the whole hex-string for blanking the screen: 307c4c00327c5b0010bc00005288b1c966f613fc00000060001813fc00000060001910390060001b0c0000ff67f44e750000
And here’s the hex-string for setting all pixels to black: 307c4c00327c5b0010bc00ff5288b1c966f613fc00000060001813fc00000060001910390060001b0c0000ff67f44e750000
I tried to implement the code with interrupts: Telling the CPU to go to sleep and wake up if a key is pressed. But the “go-to-sleep”-instruction STOP causes a “Privilege Violation”, so this busy-wait-loop is the best I can do. If I don’t call a ROM-instruction, that is…
There is a bug…
There is a weird behavior when I press keys like Esc, Apps, or one of the lower Enter keys. The top Enter key only works right at the start or if I press it for a longer time. Esc and Apps don’t trigger at all.
If you know why this happens, feel free to contact me at hello@anty.at. Thanks!
Waiting for a keypress with a ROM call
Since telling the CPU to go to sleep is a privileged instruction that we cannot execute in user mode, and checking the keyboard in a busy-waiting-loop is wasting energy, we have to resort to a ROM call. ROM-calls are provided by the calculator’s operating system and run in privileged mode, so they can stop the CPU, if they want to.
There is a ROM call called ngetchx() that waits for a keypress and returns the key number. Let’s use that call (and ignore the return value):
MOVEA.L $C8,A0 ; 2078 00C8 Load AMS Jump Table Pointer (a pointer stored at 0xC8) into A0
MOVEA.L 4*$51(A0),A1 ; 2268 0144 Load address of ngetchx() into A1
JSR (A1) ; 4E91 call ngetchx()
This code translates to 207800c8226801444e91 in hex.
The AMS Jump Table is defined at the address 0xC8. The Jump Table is a list of 4 byte pointers, pointing to hundreds of functions defined by the operating system. Since ngetchx() is defined to be at index 0x51, we multiply it by 4 bytes and get 0x144. To execute the code of ngetchx() we call JSR with the pointer to the result of 0x144 + A0.
So here are the solutions for our programs, using the ngetchx() ROM call:
Blank screen with energy efficient waiting for a keypress:
307c4c00327c5b0010bc00005288b1c966f6207800c8226801444e914e750000.
And the black screen version:
307c4c00327c5b0010bc00ff5288b1c966f6207800c8226801444e914e750000.
Finally a black screen to look at:

Use a compiler instead?
I know what you are thinking. Building that hex-string by hand is fun and all, but can’t we do this automatically? Sure!
On Linux
- Install
vasmm68k_mot,xxdandsed. - Put your Assembly code in a file, e.g. zero-screen.asm.
- Execute
vasmm68k_mot zero-screen.asm -Fbin -o zero-screen.binto compile your assembly code to a binary file called zero-screen.bin. - Execute
xxd -p zero-screen.bin | tr -d '\n' | sed 's/$/0000\n/'to get the hex-string.
Don’t like assembly? Use C!
If you don’t like writing assembly, then you want to check out TIGCC. Apparently there’s an IDE that lets you write C code, but it requires Qt 3 and that’s too old for my system so I haven’t tried it yet.
Bonus 2: My game “Crate”
If you like puzzle games, you might find my TI-92 Plus game “Crate” interesting.
You navigate a maze in top-down view by moving boxes strategically. The goal is to get a key to unlock the door to the next level.
It was written entirely in TI-BASIC 89 on the small TI-92 Plus screen. Fun times.
Instructions
Move boxes around, destroy them, and push them into holes to make your way to the exit. There are also teleporters and switches that spawn or destroy things.
Show your inventory by pressing Apps. You can collect the key and bombs.
Also, take a look at the level editor if you feel creative!