z80:Direct Input/Output

From Learn @ Cemetech
Jump to: navigation, search

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

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.

Delays

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/OUT

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 0 || DOWN || LEFT || RIGHT || UP ||~ ||~ ||~ ||~ || || 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 || || Group 5 ||~ || STO || LN || LOG || X^^2^^ || X^^-1^^ || MATH || ALPHA || || Group 6 || GRAPH || TRACE || ZOOM || WINDOW || Y= || 2ND || MODE || DEL ||

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
    rla
    rla
   
   ;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
   
   Up:
    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. ex. 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.

WaitLCD

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.

Coding

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


   fastCopy:
    di
    ld a,$80
    out ($10),a
    ld hl,gbuf-12-(-(12*64)+1)
    ld a,$20
    ld c,a
    inc hl
    dec hl
   fastCopyAgain:
    ld b,64
    inc c	
    ld de,-(12*64)+1
    out ($10),a
    add hl,de
    ld de,10
   fastCopyLoop:
    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
    ret


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

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.

[!-- I don't know a thing about linking, so could someone help here? --]

Review

Conclusion

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.