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:
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.
|