Techno-Plaza
Site Navigation [ News | Our Software | Calculators | Programming | Assembly | Downloads | Links | Cool Graphs | Feedback ]
 Main
   Site News

   Our Software

   Legal Information

   Credits

 Calculators
   Information

   C Programming

   Assembly

     Introduction

     Keyboard Input

       Part I

       Part II

       Part III

     Basic Graphics

     C & Assembly

   Downloads

 Miscellaneous
   Links

   Cool Graphs

   Feedback Form

TIGCC Assembly Lessons

TIGCC Assembly Programming Lessons

Lesson 2: The Basics of Keyboard Input

Step 4 - Keyboard Input via _rowread

Now that we've seen the benefits of other keyboard reading methods, let's go on to our final way of reading keyboard input. Directly using the hardware.

How does the AMS know a key has been pressed? It reads the keyboard matrix directly inside the automatic interrupts (functions that get called automatically by the system). The AMS actually uses these interrupts for lots of things. One annoying thing it does it draw the status bar, which at times messes up our screen when we don't want it to. Also, as we have mentioned, we cannot test for one of the modifier keys (diamond, shift, hand, alpha, 2nd) alone. Finally, all the work the AMS does in these interrupt handlers takes time. If we need more speed, a good way to get it is to disable the interrupts.

Now, we always seem to be trapped in a trade-off. One thing gets good, another thing gets bad, so let's talk about the _rowread tradeoff. The are at least three obvious downsides of using _rowread. First, it's much more complicated than using OSdequeue or ngetchx. It's much more work and requires lots of testing to make sure we're getting the results we want. Second, we have to test for each key we want to look for. Unlike ngetchx or OSdequeue which returns the TIOS key code of the key we pressed, we must test and check for each key stroke we want to know about. So, instead of calling the function and checking which arrow key's keycode we got, we need to test for each arrow to see if it was pressed. Fortunately, it's rare that we need to use many keys on the keyboard anyway. Most game programming (the most common reason for direct keyboard input) uses only a few keys, just like a Nintendo controller only had 4 buttons, how many do we really need for a calculator? The final downside? The keyboard on the TI-89 is different from the keyboard on the TI-92+/V200. Making compatible programs will often entail making two versions of the programs, or using macros to test which calculator the program is running on, and then adapt for that calculator. Fortunately, we only need to modify small parts of the code to make it compatible with both calcs, so this is only a minor hassle.

Well, let's see an example. Start TIGCC and create a new project. As usual we need a C source file named main, and a GNU assembly file. You can name this rowread. Save the project as rowread and edit the assembly file as follows. You should already have the source for the main.c file, but here is the link if you don't.


    .include "os.h"

    .equ AMS_jumptable,0xC8
    .equ ARROW_ROW,~0x0001
    .equ AUTO_INT_1,0x64
    .equ AUTO_INT_5,0x74

    .equ USE_TI89,1

    .ifne USE_TI89
        .equ KEY_UP,0
        .equ KEY_LEFT,1
        .equ KEY_DOWN,2
        .equ KEY_RIGHT,3
        .equ KEY_2ND,4
        .equ KEY_SHIFT,5
        .equ KEY_DIAMOND,6
        .equ KEY_ALPHAHAND,7
        .equ KEY_ESC,0
        .equ ESC_ROW,~0x0040
    .else
        .equ KEY_2ND,0
        .equ KEY_DIAMOND,1
        .equ KEY_SHIFT,2
        .equ KEY_ALPHAHAND,3
        .equ KEY_LEFT,4
        .equ KEY_UP,5
        .equ KEY_RIGHT,6
        .equ KEY_DOWN,7
        .equ KEY_ESC,6
        .equ ESC_ROW,~0x0100
    .endif

    .text
    .xdef asm_main

|-------------------------------------------------------------------------------
| ams_main - our program starts here
|

asm_main:
    movem.l %d3-%d6/%a5,-(%sp)      | save registers
    movea.l AMS_jumptable,%a5       | load the AMS jumptable

    movea.l 4*ClrScr(%a5),%a0       | ClrScr();
    jsr     (%a0)

    movea.w #AUTO_INT_1,%a0         | save auto-interrupt 1 in %d3
    move.l  (%a0),%d3

    bclr.b  #2,0x600001             | redirect auto-interrupt 1
    move.l  #dummy_handler,(%a0)
    bset.b  #2,0x600001

    movea.w #AUTO_INT_5,%a0         | save auto-interrupt 5 in %d4
    move.l  (%a0),%d4

    bclr.b  #2,0x600001
    move.l  #dummy_handler,(%a0)    | redirect auto-interrupt 5
    bset.b  #2,0x600001

    clr.w   %d5                     | done = 0;

    move.w  #1,-(%sp)               | print_str(50, message, 1);
    pea     message(%pc)
    move.w  #50,-(%sp)

    bra     print_str

while:
    move.w  #ARROW_ROW,%d0          | %d0 = _rowread(ARROW_ROW);
    bsr     _rowread

    move.w  %d0,%d6                 | save results in d6

    btst.b  #KEY_LEFT,%d6           | test for arrows
    bne     found_arrow

    btst.b  #KEY_RIGHT,%d6
    bne     found_arrow

    btst.b  #KEY_DOWN,%d6
    bne     found_arrow

    btst.b  #KEY_UP,%d6
    bne     found_arrow

    bra     check_stat_keys         | if no arrows found, check for stat keys

found_arrow:
    move.w  #4,-(%sp)               | print_str(0, arrow_str, 4);
    pea     arrow_str(%pc)
    clr.w   -(%sp)
    bra     print_str

check_stat_keys:
    btst.b  #KEY_2ND,%d6            | test for 2nd key
    beq     check_shift             | if not, goto check_shift

    move.w  #4,-(%sp)               | print_str(0, second_str, 4);
    pea     second_str(%pc)
    clr.w   -(%sp)
    bra     print_str

check_shift:
    btst.b  #KEY_SHIFT,%d6          | test for Shift key
    beq     check_diamond           | if not, goto check_diamond

    move.w  #4,-(%sp)               | print_str(0, shift_str, 4);
    pea     shift_str(%pc)
    clr.w   -(%sp)
    bra     print_str

check_diamond:
    btst.b  #KEY_DIAMOND,%d6        | test for diamond key
    beq     check_alphahand         | if not, goto check_alphahand

    move.w  #4,-(%sp)               | print_str(0, diamond_str, 4);
    pea     diamond_str(%pc)
    clr.w   -(%sp)
    bra     print_str

check_alphahand:
    btst.b  #KEY_ALPHAHAND,%d6      | test for alpha key
    beq check_esc                   | if not, goto check_esc

    move.w  #4,-(%sp)               | print_Str(0, alpha_str, 4);
    pea     alpha_str(%pc)
    clr.w   -(%sp)
    bra     print_str

check_esc:
    move.w  #ESC_ROW,%d0            | %d0 = _rowread(ESC_ROW);
    bsr     _rowread

    btst.b  #KEY_ESC,%d0            | check for ESC key
    beq     while                   | if not, loop

    moveq   #1,%d5                  | done = 1;

    move.w  #1,-(%sp)               | print_str(10, esc_str, 1);
    pea     esc_str(%pc)
    move.w  #10,-(%sp)
    bra     print_str

end_while:
    tst.w   %d5                     | if done == 0
    beq     while                   | goto while

    movea.w #AUTO_INT_1,%a0         | restore auto-interrupt 1
    bclr.b  #2,0x600001
    move.l  %d3,(%a0)
    bset.b  #2,0x600001

    movea.w #AUTO_INT_5,%a0         | restore auto-interrupt 5
    bclr.b  #2,0x600001
    move.l  %d4,(%a0)
    bset.b  #2,0x600001

    movea.l 4*ngetchx(%a5),%a5      | ngetchx(); ngetchx();
    jsr     (%a5)
    jsr     (%a5)

    movem.l (%sp)+,%d3-%d6/%a5      | restore registers
    rts

print_str:
    clr.w   -(%sp)

    movea.l 4*DrawStr(%a5),%a0
    jsr     (%a0)                   | DrawStr(0,y,str,type)

    adda.l  #10,%sp                 | reset stack pointer
    bra     end_while               | goto end_while

dummy_handler:
    rte                             | return from interrupt

|-------------------------------------------------------------------------------
| Data Section
|
    .data

arrow_str:
    .string "Arrow key pressed...   "

diamond_str:
    .string "Diamond key pressed..."

second_str:
    .string "2nd key pressed...    "

alpha_str:
    .string "Alpha key pressed...  "

shift_str:
    .string "SHIFT key pressed...  "

esc_str:
    .string "Escape? Goodbye..."

message:
    .string "Press any key. ESC to quit"

Save the project and build the program. Send it to TiEmu. It will look something like this:

TI-89 AMS 2.05 rowread.89z

Step 4a - Program Analysis

Let's start with an overview of what the program does. The program is continually reading the keyboard checking for certain keys. It looks for the four arrow keys, the the four modifier keys (2ND, Diamond, Shift, and Alpha/Hand depending upon TI-89 or TI-92+/V200). If it finds one, it will print a string at the top. Then it checks for the ESC key, if it is pressed, we display a string, and exit the program. The program will keep looping until ESC is pressed.

We have a few new things at the top that I want to cover, so we'll start there.

    .equ USE_TI89,1

    .ifne USE_TI89
        .equ KEY_UP,0
        .equ KEY_LEFT,1
        .equ KEY_DOWN,2
        .equ KEY_RIGHT,3
        .equ KEY_2ND,4
        .equ KEY_SHIFT,5
        .equ KEY_DIAMOND,6
        .equ KEY_ALPHAHAND,7
        .equ KEY_ESC,0
        .equ ESC_ROW,~0x0040
    .else
        .equ KEY_2ND,0
        .equ KEY_DIAMOND,1
        .equ KEY_SHIFT,2
        .equ KEY_ALPHAHAND,3
        .equ KEY_LEFT,4
        .equ KEY_UP,5
        .equ KEY_RIGHT,6
        .equ KEY_DOWN,7
        .equ KEY_ESC,6
        .equ ESC_ROW,~0x0100
    .endif

As we discussed above, the TI-89's keyboard is different from the TI-92+/V200. To make the program work on the TI-89, we need different constants than on the TI-92+/V200. This is one way to get our separate values. We start by defining a constant USE_TI89 and set it equal to 1. If this constant is 1, we will use the TI-89 keyboard values, if it's 0, we will use the TI-92+ values.

Next we have a new directive, the .ifne directive. It stands for IF Not Equal to zero. So, if USE_TI89 is not equal to zero, we will define all the constants until the .else directive. Otherwise, we define the ones in the .else. Note that if you compile the TI-92+ version and send it to your TI-89, you will have no way to exit the program and will have to reset your calc.

    movea.w #AUTO_INT_1,%a0         | save auto-interrupt 1 in %d3
    move.l  (%a0),%d3

    bclr.b  #2,0x600001             | redirect auto-interrupt 1
    move.l  #dummy_handler,(%a0)
    bset.b  #2,0x600001

    movea.w #AUTO_INT_5,%a0         | save auto-interrupt 5 in %d4
    move.l  (%a0),%d4

    bclr.b  #2,0x600001
    move.l  #dummy_handler,(%a0)    | redirect auto-interrupt 5
    bset.b  #2,0x600001

Skipping down a little, we come to the asm_main function just after the ClrScr() call. Here is where we disable our interrupts. We could actually turn interrupts off, but this is a bad idea because the clock needs auto-interrupt 3 to work, and auto-interrupt 3 doesn't hurt us. So instead of turning them off, we will just replace them temporarily.

The first and fifth auto-interrupts can interfere with keyboard reading, so these are the ones we need to replace. The addresses of the auto-interrupts are stored at fixed locations, just like the AMS jumptable address. The TIGCC docs has these defined in the IntVecs enumeration. You can see we have added .equ directives for AUTO_INT_1, and AUTO_INT_5.

So, the first step is to save the old addresses so we can restore them later. If we don't, the calc will stop working. So, move the location of the AUTO_INT_1 into a0, then move what a0 is pointing at (the real address) into d3.

The next part is a little confusing. This is the code for the SetIntVec function from TIGCC. However, it's not actually a function, it's a macro, which means we cannot call it like we can AMS functions or other TIGCC functions. So we have to import the code here.

We introduce the bclr (Bit CLeaR) instruction. As you may surmise, bclr clears one of the bits in the destination. Clearing bit 2 of address 0x600001 turns off the protected memory interrupt. If we don't do this, the calculator will call this interrupt and crash. This is because we are trying to write to a protected memory address.

After we clear the bit, we can move the new auto-interrupt function's address into place. Putting a # sign in front of the dummy_handler label will get the address of the dummy_handler function, and this is what we need to move into the AUTO_INT_1 address, so that our dummy_handler will get called instead of the normal AUTO_INT_1. Let's take a quick look at dummy_handler real quick. It's defined at the bottom.

dummy_handler:
    rte                             | return from interrupt

As we can see, it's a very short function. It has only one opcode, the rte instruction. rte (ReTurn from Exception) is similar to rts, but it is specially used for returning from interrupt handlers. I don't plan to go into much detail here. You will only ever use this in an interrupt handler, which is to say not that often.

After we have moved our new dummy_handler address into the auto-interrupt, we just need to set the bit we cleared on 0x600001. bset (Bit SET) is the opposite of bit clear. AUTO_INT_5 is handled just like AUTO_INT_1, so let's move on.

    clr.w   %d5                     | done = 0;

    move.w  #1,-(%sp)               | print_str(50, message, 1);
    pea     message(%pc)
    move.w  #50,-(%sp)

    bra     print_str

This code is pretty familiar. The only thing of note here is that we have put part of the code for the call to DrawStr in its own place, the print_str label. We will be using DrawStr a lot in this program, so we saved a little space by doing this. Let's take a look at the print_str label. It's towards the bottom of the file.

print_str:
    clr.w   -(%sp)

    movea.l 4*DrawStr(%a5),%a0
    jsr     (%a0)                   | DrawStr(0,y,str,type)

    adda.l  #10,%sp                 | reset stack pointer
    bra     end_while               | goto end_while

As you can see, we have already pushed the last three arguments to DrawStr, and the first one is always 0 for us. At the end of print_str, we branch to end_while. Let's go there now.

end_while:
    tst.w   %d5                     | if done == 0
    beq     while                   | goto while

At the end_while label, we check to see if d5 is 0. We are using d5 to tell us if we are done or not. Since we just cleared it a moment ago, it will still be 0 here. Let's finish end_while before we get to while.

    movea.w #AUTO_INT_1,%a0         | restore auto-interrupt 1
    bclr.b  #2,0x600001
    move.l  %d3,(%a0)
    bset.b  #2,0x600001

    movea.w #AUTO_INT_5,%a0         | restore auto-interrupt 5
    bclr.b  #2,0x600001
    move.l  %d4,(%a0)
    bset.b  #2,0x600001

Before we exit the program, we need to restore the auto-interrupts. We are basically reversing the process above. Remember that we saved the interrupts in the d3 and d4 registers, so we just move them back.

    movea.l 4*ngetchx(%a5),%a5      | ngetchx(); ngetchx();
    jsr     (%a5)
    jsr     (%a5)

This is a little weird for two reasons. First, we moved the ngetchx function into a5. It is perfectly valid to overwrite the contents of the register in a move. So we can locate the function using the AMS_jumptable pointer stored at a5, then overwrite it an put it back in a5.

There are two reasons I did this. You may have noticed in the print_str function that I used a0 to call DrawStr. However, if I use a0 here, because a0 will get destroyed by the call to ngetchx, I'll have to move ngetchx back to a0 again before I can call it the second time. Since we have no more use for the AMS_jumptable pointer, we'll just overwrite it so we don't have to save any other registers.

Now, for the reason we called it twice. Well, the keyboard works by sending electric currents. If we check the keyboard too quickly, we may think the same key was pressed more than once, even though in reality, the keyboard just didn't have time to dissipate the current. Once we have pressed ESC to exit and turn the interrupts back on, the AMS will read the keyboard and see the ESC key. This will be immediately returned by ngetchx and the program will exit. We need to call it twice to make sure we actually end up waiting for a key.

One other idea we could have used was putting in some slow down code to give the keyboard time to cool down so the AMS wouldn't see the ESC key, but it's difficult to time such things. This is easier. Okay, let's move on to while.

while:
    move.w  #ARROW_ROW,%d0          | %d0 = _rowread(ARROW_ROW);
    bsr     _rowread

    move.w  %d0,%d6                 | save results in d6

    btst.b  #KEY_LEFT,%d6           | test for arrows
    bne     found_arrow

Okay, here is where we start using _rowread. _rowread is a TIGCC library function. If you look in the TIGCC docs, you will see it takes an unsigned short (word-size value) and returns an unsigned short. The calling convention of the _rowread function (in TIGCC 0.96b8, it has been different in the past) is that the value is passed in d0 and returned in d0.

To do low-level keyboard reading with rowread, we need to know about the TI-89/92+/V200 keyboard. You can find the keymatrix in the TIGCC docs in the _rowread function doc. We see that it requires you to pass the inverse of the key row you want to read. In our case, we want to read row 0, which I call the ARROW_ROW (because all the arrow keys are on it). You can see that ARROW_ROW is defined at the top as ~0x0001. The 0x0001 is hex for the 0 bit, and the ~ gives us the inverse, which is actually 0xFFFE. When _rowread returns, we will have the column in d0, which we can check for specific keys.

All the KEY_XX values we defined are bit numbers. They correspond to the column bit in the keymatrix. On the TI-89, the KEY_LEFT is bit 1. To check whether this bit is set or not, we use the btst (Bit TeST) instruction. It will set the status register's Z (zero) flag according to the result of this test. If we are not zero, then we have pressed the key.

check_stat_keys:
    btst.b  #KEY_2ND,%d6            | test for 2nd key
    beq     check_shift             | if not, goto check_shift

Most of the code following is pretty much the same as we have already seen. I just wanted to point out this real quick. The beq (Branch if EQual to zero) instruction. All the condition code branching opcodes are documented on page 129 of the Motorola Programmer's Reference. You might want to take a look at the various condition codes and their instructions. In this case, beq is the opposite of bne.

check_esc:
    move.w  #ESC_ROW,%d0            | %d0 = _rowread(ESC_ROW);
    bsr     _rowread

    btst.b  #KEY_ESC,%d0            | check for ESC key
    beq     while                   | if not, loop

    moveq   #1,%d5                  | done = 1;

I'm going to skip down to the check_esc label now as everything up till then is basically the same. Since the ESC key is not on the same row as the arrows and the modifier keys, we will need to do another _rowread.

If we get the ESC key, we will move 1 into d5. This will tell the end_while loop that we are ready to exit the program.

Step 5 - Conclusions

I hope these examples have been insightful. You now have the best methods of keyboard input, depending upon what you need to do. A couple last notes on _rowread and interrupts. You cannot use OSdequeue or ngetchx with interrupts disabled. Also, _rowread() is so fast that you may read the same key press more than once. It may be necessary to slow your program down so you are only responding to one key press.

 


Lesson 2: The Basics of Keyboard Input
Questions or Comments? Feel free to contact us.


Copyright © 1998-2007 Techno-Plaza
All Rights Reserved Unless Otherwise Noted

Get Firefox!    Valid HTML 4.01!    Made with jEdit