Direct Input/Output


One of the benefits of assembly programming is that you can directly access the hardware of your calculator. This allows you to create your own GetKey routines, draw directly to the LCD (yes, directly instead of having to use a ROM call), and many other things that directly modifying/reading from hardware allows you to do.


Ports are entry points that you can read and write to the hardware from. Each port has an address number, and to access the port you must that port's address number. Note that some hardware items have multiple ports. For a list of ports and the corresponding hardware item, see this page.


One thing to keep in mind before using direct input/output is that hardware is slow (compared to the processor, at least). If your program does not work when you try to run it, the problem may be that you did not allow the hardware enough time to respond to the processor's commands. In order to give it time, you may (or may not, depending on the specific hardware) have to tell the processor to do something that's a total waste of time and space to give the hardware time to catch up.

A good delay should wait only for the minimum amount of time required for the hardware to catch up, be as small as possible, and at best do something useful, though sometimes you'll have extra time that you must waste with useless instructions. It should also leave anything you want intact in the same state after you run the delay.

;example delay
 push hl
 push de
 pop de
 pop hl


In order to access hardware, you must send and receive bytes. The only way to do this is through the IN and OUT instructions, or their variations (INI, OUTI, IND, OUTD, INIR, OTIR, INDR, or OTDR.

Direct Input

Direct input is taking useful information from an input device (keyboard, link port, etc.), and using it for useful means.

The Keyboard

Most of the time direct input refers to the keyboard. After all, it is the most common method of user input. So, how do we get direct input from the keyboard? The keyboard port is $01, so to get data from the keyboard port it makes sense we would want to do in a,($01), right? Unfortunately, if it was this simple there would be no need for this discussion, and we wouldn't need the GetKey or GetCSC ROM calls.

Since the keyboard is an extremely simple device, it returns a bit if a key is pressed, the bit returned corresponding to the key pressed. Now, instead of assigning each key an arbitrary number, the keyboard driver divides the keys into groups, returning only the keypress bits of those in an activated group. Here is a table of key bits and their corresponding group.

%11111110 %1111101 %11111011 %11110111 %111011111 %11011111 %10111111 %01111111
Group 1 ENTER + - × ÷ ^ CLEAR
Group 2 (-) 3 6 9 ( TAN VARS
Group 3 . 2 5 8 ) COS PRGM STAT
Group 4 0 1 4 7 , SIN APPS X, T, θ, n

So, to check to see if a key was pressed, we would need to activate a group and then check to see if the key was pressed. To activate a group, we'll reset the bit of the group. So, to activate the group that contains the arrow keys, we'll need to reset the zeroth bit.

ld a,%11111110        ;we only want the arrow keys activated, so reset bit 0
out ($01),a

The keyboard isn't one of the slower drivers, so we won't need a delay. We'll just test to see if any keys were pressed.

in a,($01)

The value contained in a now equals the corresponding value for that key. So, if down was pressed, a=%11111110. Note that when you execute IN a,($01), it does not care what group you're currently in. You need to keep track of that so if down is pressed, the program doesn't interpret it as an enter.

bit 0,a        ;check bit 0 to see if down was pressed

Multiple Key Presses

In the Same Group

So what happens if in the above scenario you press both up and down? a would bitwise AND the two values together.

;what happens when user presses up and right at the same time
 ld a,%11111110        ;activate arrow keys
 out (%01),a
;keys pressed
 in a,(%01)
;a= bitwise AND of two values, %11001111

Remember earlier when we used BIT to see if a key was pressed? This is the reason why. If multiple keys were pressed, and you cp a,%11101111, the calculator will tell you that up wasn't pressed, even though it was. There is only one time you would want to use CP: when you want the user to only press one key. The question may arise why, but that won't be discussed here.

In Different Groups

So what if you want to see if 2nd and up were pressed? you couldn't, right? they're in different groups, so whichever group you activated, one keypress wouldn't be returned. What if you activated both? That's perfectly legal hardware wise, so let's do it!

;activate both groups at the same time!
 ld a,%10111110
 out ($01),a

 in a,($01)
 cp %11001111        ;if 2nd and Up were both pressed, a would equal this, right?

Seems logical, right? Wrong. What happens if Y= and 2nd were pressed? Oh, you happen to get the same value returned! So if this doesn't work, what do we do now?

The answer is to quickly 'swap' groups. You activate one group, test for a key, and then quickly swap to the other group and test for the other key.

;we'll test for 2nd first
 ld a,%101111111
 out ($01),a

 in a,($01)
 or %11011111        ;only 2nd bit will be reset now, if it was pressed
 rla                ;rotate that value into the Carry flag

;testing for up
 ld a,%11111110
 out ($01),a

 in a,($01)
 bit 4,a            ;check to see if up was pressed
 jr z,up

 ...                ;other code

 jr c, no2nd            ;if carry was set, then 2nd wasn't pressed

Seems considerably longer, doesn't it? Sorry, you'll have to get use to it. It doesn't work any other way. The good thing is that it works with all keys, any combination, and even more than two keys being pressed.

Direct Output

Direct output is to change the hardware in a useful way to give feedback to the user. The most common of these are the link port and the LCD driver.

The LCD Driver

Similar to Direct Input referring to the keyboard, direct output for the most part refers to the LCD driver. The two ports that the LCD driver uses are ports $10 and $11.

Port $10: LCD Status Port

Port $10 is the LCD status port, and it does many things that deal with what the LCD is doing, including what row/column is currently being pointed to, turn on/off the LCD, set contrast, change auto-increment method, and many other things.

Reading from

Reading from port $10 returns these values:

  • Bit 0: Set if auto-increment mode (commands 05 or 07) is selected, reset if auto-decrement mode (commands 04 or 06) is selected.

This is just a test to see if the LCD is in auto-increment or auto-decrement mode. What this means is that every time you read/right (except for the dummy read) to the LCD data port, the pointer will either move forwards or backwards, up or down depending on what was set. Default for TI-83 plus series calculators is X-auto increment mode.

note that from just bit 0 alone, you can not determine the direction of change, only if it is incrementing or decrementing.
set: moving pointer in down or right direction
res: moving pointer in up or left direction
It is also important to note that you can only have 1 mode at a time, so it can't be moving in the up-left direction, etc.

  • Bit 1: Set if auto-increment or auto-decrement will affect the current column, or reset if auto-increment/decrement will affect the current row.

Determines the direction of auto-increment/decrement. It does not determine if it is incrementing or decrementing, only which direct it is happening in (horizontal or vertical).

horizontal: set
vertical: reset

  • Bit 2, 3: Not used.
  • Bit 4: Set if in reset state, reset if in operating state.

In reset state, the LCD driver has to "reset itself" before the next use, and is quite slow at doing so. When it is ready to be used again (either by writing to either port $10 or port $11 or reading from port $11), it is operating state.
Note: If you try to use the either LCD ports before the LCD driver is ready again, it will most likely result in garbage being read or written.

  • Bit 5: Set if the display is enabled. Reset if disabled. (Note: LCD is completely turned off via Port 03h.)
  • Bit 6: Set if the LCD will transfer 8 bits at a time through Port 11. Reset if the LCD will only transfer 6 bits at a time.

The LCD can be set to be either in 6 bit mode or 8 bit mode. All this means is whether the LCD will read 'bytes' either as 6 bits or 8 bits.

  • Bit 7: Set if the LCD is busy. Reset if a command can be accepted.

A very useful thing because the LCD is exceedingly slow. If the LCD is busy, any command given will be "ignored", or misinterpreted. Either way, it's not good, so we want a way to make sure that we can read and write to the LCD.


There is a call that will wait for the LCD to not be busy. It is WaitLCD. However, it was not originally implemented, so TI stuck it in a random location of ram and even left off the definition. For this reason, WaitLCD should not be treated as a ROM call. Instead, you'll need to define the area in memory it's located ($000B), and call it. It does not affect any flags, registers, or whatnot.

.define waitLCD $000B

 ld a,$FF
 call $000B
 out ($11)

Don't worry what this does for now, just know that you have the necessary delay in between writing to the LCD. This can be done by having filler instructions, whether they do something useful or not, but WaitLCD is a safe bet because it will always wait for at least the minimum time required (usually more, though not a whole lot).

Writing to

These are the effects of writing values to Port $10:

  • $00: Switch to 6-bit mode. (Port $11 transfers 6-bits at a time.)

This can be though of as "Text mode" or "Home screen mode". It's used for displaying the big text on the Home screen. Generally, you probably don't want to enter this mode yourself because the ROM calls will do it for you. Note that the display isn't affected by writing this value to the port (as in you won't see anything different)! The difference is when you try writing to /reading from Port $11, it will either ignore the 6th and 7th bits, or return them as always being zero.

  • $01: Switch to 8-bit mode. (Port $11 transfers 8-bits at a time.)

The reverse of writing $00 to port $10. 'nough said.

  • $02: Disable the screen.

This blanks the screen and disconnects the LCD RAM from the physical screen, thus allowing you to use the LCD RAM as extra saferam. However, writing to LCD RAM has to be done through port $11, meaning that it will be extremely slow.

  • $03: Enable the screen.

This resumes displaying the LCD's RAM contents to the physical screen. Note that it will immediately update the display with whatever was in the LCD RAM, and is recommended that you use ClrLCDFull if you had used the LCD RAM as saferam.

  • $04: Set X auto-decrement mode.

Every read or write operation from the data port will cause the LCD's internal pointer to move up one row.

  • $05: Set X auto-increment mode.

Every read or write operation from the data port will cause the LCD's internal pointer to move down one row. The TI-83+ expects the LCD to be in this mode for most display routines. They also generally don't reset back to this mode, so you must do so yourself.

  • $06: Set Y auto-decrement mode.

Every read or write operation from the data port will cause the LCD's internal pointer to move left one column

  • $07: Set Y auto-increment mode.

Every read or write operation from the data port will cause the LCD's internal pointer to move right one column.

  • $08-$0B: Set power supply enhancement.

You'd be well advised to just leave this alone. Basically the amount of power being sent to the LCD.

  • $10-$13: Set power supply level.

You'd be well advised to just leave this alone. Basically the amount of power being sent to the LCD.

  • $14-$17: Undefined.
  • $18: Cancel test mode.

Test mode is a mode of "Super energy" to see if the LCD is working. It will cause rows of pixels to be turned dark blue. However, it's use is discouraged because it will likely damage your LCD permanently.

  • $19-$1B: Undefined.
  • $1C-$1F: Enter test mode.

See above comments about "Canceling test mode".

  • $20-$3F: Set column.

$20-$2E are valid columns in 8-bit mode, $20-$33 are valid columns in 6-bit mode. $34-$3F values are also accepted, but do not generally correspond to a drawable area (see "Z addressing" comments).

  • $40-$7F: "Z addressing"

It just changed what physical row the top row of RAM is displayed on. The LCD will wrap the bottom of it's RAM to the top of the screen as needed. It provides a kind of "screen shift operation" if you had data on the previously un-drawable areas of LCD RAM.

  • $80-$BF: Set row.
  • $C0-$FF: Set contrast.

$C0 is the lowest ("Lightest") contrast mode.. Note that there is a System RAM area (contrast) for this which will need to be updated if you want 2nd+Up/2nd+Down to change the contrast as expected (instead of causing a sudden jump…).

Port $11: LCD Data Port

The data port is where you write to/read from the LCD RAM.

Writing to this port writes the value to the current LCD pointer, which generally translates to what the screen draws at the LCD pointer.

Reading returns the byte in the LCD RAM at the current LCD pointer .

Reading from/writing to this port will also change the LCD pointer depending on the mode you set in port $10.


So now let's write some code that draws directly to the screen (or rather, look at someone else's for now).

 ld a,$80
 out ($10),a
 ld hl,gbuf-12-(-(12*64)+1)
 ld a,$20
 ld c,a
 inc hl
 dec hl
 ld b,64
 inc c    
 ld de,-(12*64)+1
 out ($10),a
 add hl,de
 ld de,10
 add hl,de
 inc hl
 inc hl
 inc de
 ld a,(hl)
 out ($11),a
 dec de
 djnz fastCopyLoop
 ld a,c
 cp $2B+1
 jr nz,fastCopyAgain

Recognize this? Yes, this is the famous ionfastcopy! The mystery behind how things actually get onto the LCD display isn't that much a mystery anymore, huh :)?

Last Remarks

A few things to keep in mind when writing code dealing with directly drawing/reading from the LCD:

  • Remember to disable interrupts. They can mess up the writing/reading process.
  • Remember which increment mode you're in, and change it to your advantage. Also, don't forget to reset it back to it's default (X auto increment) before quitting or running a TI ROM call that draws something to the screen.
  • Remember to allow sufficient delays between writes/reads. You can either have useful instructions or time wasting instructions running during this time.
  • Reading from port $10 does not put the LCD driver into reset mode.


Linking is in a category of it's own. The link can act as both a direct input or output source, via sending/receiving files. For more information see here.



What about Direct Input/Output in other ports? Surely you must be able to do the same thing with each and every one of the other ports! The answer is you can. However, it would be easier to direct you to the page on ports than explain them all here. Hopefully after some careful thought and consideration, you'll realize that ports work almost exactly the same, and the only difference is their function.

Unless otherwise stated, the content of this page is licensed under GNU Free Documentation License.