This program introduces a new concept in TI programming: low level
keyboard reading. Instead of using the operating system to read the keys
for us, we can read the keys directly from the keyboard matrix. Every
time you press a key on your TI-89/92+/V200, it sends an electrical
signal corresponding to the key you just pressed. This signal lasts a
few hundredths of a second. Normally, we use the AMS to read these
electrical signals and interpret them for us. But this is slow. It would
be impossible to rely on this if you were trying to program an action
game.
Now, we know that the keyboard uses a matrix like structure to represent
the various electrical signals generated by the keyboard. But another
concept in the hardware design is memory mapped I/O. This means that a
certain memory address corresponds to a certain device on the
calculator. The screen memory works this way, and so does the keyboard.
This is very nice, because it means we can read a memory address
directly in software, but those memory addresses directly reflect the
hardware. This makes software/hardware interaction very very fast. So,
low level keyboard reading is the fastest way to use the keyboard. So
fast in fact, that we must slow down the program or we will be reading
the same key press more than once. No, I'm not kidding. The delay()
function in the program was written directly for this reason.
So, how does the keyboard reading work? Well, that's easy. We use a
function called _rowread(), which reads one of the rows of the keyboard
matrix and returns that row. To read the row, we have to tell _rowread()
which row to read. This is where our first bit mask comes in, as you
will see. We want to mask out the row we want to read (this is also
known as an inverse bit mask, because we are masking out the data we
want, rather than masking out the data we don't want, but its usage is
the same). The _rowread() function returns the row of the keyboard
matrix we want to check. This is our position variable. So, the only
thing left to do is perform bitwise AND on the keyboard row (returned by
_rowread()), and another bit mask which will be the key we want to
check for. The keys come from the columns of the keyboard matrix. The
keyboard matrix is displayed in the TIGCC docs in the kbd.h header file.
So, if we do bitwise AND with our keyboard row (from _rowread()), and
the bit mask column (which we have to store in a variable from the
definition in the TIGCC docs), we can see if a key was pressed or not.
This nice thing here is that we can read any key, including the 2nd,
shift, and diamond keys. And we can see if a key was pressed in
combination with another key, because it's just a collection of
electrical signals. You will see in our program how we can hold arrow
key combinations down to move up-left, or down-right, or up-right, or
down-left. You can see how this has many natural advantages. Okay, let's
start the analysis.
Since this lesson was written, a newer function was introduced in the
TIGCC library called _keytest. It has a slightly easier format for
reading the key matrix and you will probably want to look at this in the
TIGCC docs.
short int keys[4];
short int calc = CALCULATOR, key;
INT_HANDLER interrupt1 = GetIntVec(AUTO_INT_1); // save auto-interrupt 1
INT_HANDLER interrupt5 = GetIntVec(AUTO_INT_5); // save auto-interrupt 5
POSITION pos = {0,0};
SCREEN screen;
Our variable list has some new items on it, so let's go over it real
quick. The keys[] array is a short integer array, something we haven't
used much, but it is no different than any other array. Remember that
keys[4] means 4 elements total, keys[0],keys[1],keys[2],keys[3]. We use
two other short integers, the key integer for our keyboard row. This is
what we will use to test the keys using _rowread(). The other variable
calc is assigned the value of a pseudo-constant CALCULATOR, which will
be 0 if the calculator we are running is a TI-89, and will be a non-zero
value for the TI-92+/V200.
We use two different structures, a POSITION structure which keeps track
of our sprites position on the screen. You can see that we have
initialized this structure with the values (0,0) using the = { }
structure assignment operator. If we have a structure, (or an array for
that matter), we can initialize the values of the structure with
constants. The order of assignment will be the same as they are defined
in the structure. So, the x position will be initialized to 0, then the
y position will be initialized to 0.
Next we have a SCREEN structure which keeps track of our screen size.
This is because we need to account for the different screen sizes
between the TI-89 and TI-92+/V200. We use a pseudo-constant to find
these, but this pseudo-constant is not really a constant, it is a
piece of code which has to check the calculator every time it's used.
So, instead of doing that, we will assign the value we get returned by
the pseudo-constants to variables used within the structure. We cannot
assign pseudo-constants using the = { } operator. This is because the
code cannot be evaluated at compile time, so there is no way to fill the
values of that structure properly. We will have to assign them later.
The last thing to notice is a new type called INT_HANDLER. Remember that
we said we rely on the AMS to do normal keyboard reading (for ngetchx()
and kbhit(), and all those functions we learned in lesson 2). Well, the
AMS keyboard handling routines might interfere with our own low level
keyboard reading operations. It will cause odd things to happen in our
program, so we need to redirect the part of the AMS that does this until
we are finished with the program. To do that, we have save the code that
the AMS uses to perform its keyboard operations, and replace it with our
own code. However, since there is nothing we want to replace it with, we
can use dummy code written by the TIGCC creators to replace the AMS code
with nothing. But the first step in this process is to save the code the
AMS uses, and we do that with the GetIntVec() function (short for Get
Interrupt Vector). An interrupt vector is a piece of code which is
called automatically by a system at specific times. AUTO_INT_1
(automatic interrupt 1) 'fires' (runs) approximately 395 times a second.
It takes care of keyboard reading, input handling, screen updates and
other functions. These things are often very annoying, so it is nice to
be able to get rid of them at times. AUTO_INT_5 is used by the system
timers, but can also affect keyboard reading, so we will disable this
as well. First we save the code (because we will need to restore it
before we exit the program) in an INT_HANDLER (interrupt handler). This
is what that line of code does.
Remember that disabling the interrupt like this will make the AMS
functions that use input useless. You won't be able to use ngetchx(), or
kbhit(), or any of the dialog box functions unless you re-enable the AMS
interrupt handlers. We will see how this is done later.
// initialize the screen dimensions
screen.width = LCD_WIDTH;
screen.height = LCD_HEIGHT;
Okay, since we couldn't initialize this in the declaration, we will go
ahead and initialize it here. The LCD_WIDTH constant will evaluate to
either 240 or 160, depending upon the calculator. The LCD_HEIGHT will be
either 128 or 100, corresponding to the number of pixels of height. You
should remember these constants from lesson 3, but now we are keeping
them inside a variable to access them faster. It's more efficient to
evaluate the constants once, then pass their values on through the
program.
// get the correct key masks based on which calculator we have. The TI-89
// has a different keyboard mapping than the TI-92+
getKeyMasks(keys,calc);
This next part of the code is a function for finding the correct key
mask values for the arrow keys. The keyboard matrix for the TI-89 is
slightly different than the matrix for the TI-92+/V200, obviously
because they have very different keyboards. So to be compatible with
both calculators, we need to save the values for the correct key masks.
This is why we used the CALCULATOR pseudo-constant to find out which
calculator we are running on. This is very important when trying to make
programs more compatible.
void getKeyMasks(short int *keys, short int calc) {
// find the correct key masks based on which calculator we have
if (calc == 0) { // do we have a TI-89
keys[0] = 0x0001; // bit 0
keys[1] = 0x0004; // bit 2
keys[2] = 0x0002; // bit 1
keys[3] = 0x0008; // bit 3
} else { // then we must have a TI-92+
keys[0] = 0x0020; // bit 5
keys[1] = 0x0080; // bit 7
keys[2] = 0x0010; // bit 4
keys[3] = 0x0040; // bit 6
}
}
To get the correct values, we give it the keys[] array we defined above,
and the calc variable so it can tell which calculator we have. Although
the keyboard row for the arrow keys is the same on both calculators, the
bit position is not. The UP, LEFT, DOWN, RIGHT arrows on the TI-89 are
the bits 0, 1, 2, 3 respectively. But on the TI-92+/V200, they are bits
5, 4, 7, and 6 respectively. This is why we had to use the arrow key
pseudo-constants KEY_LEFT, KEY_RIGHT, KEY_UP, and KEY_DOWN in the
programs from lesson2 so it would work with both calculators.
Since we cannot use binary numbers in C, we have to define our key masks
in hex. So, we need to convert our binary numbers to hex. Bit 0
(0b00000001) is 0x0001. Bit 1 (0b00000010) is 0x0002, bit 2 (0b00000100)
is 0x0004, bit 3 (0b00001000) is 0x0008, bit 4 (0b00010000) is 0x0010,
bit 5 (0b00100000) is 0x0020, bit 6 (0b01000000) is 0x0040, and bit 7
(0b10000000) is 0x0080. Remember that we start counting at 0, so the
right most bit is bit 0, not bit 1. We need to define an entire short
integer (2 bytes) as our mask, even though we only use 8 bits of the
mask (a single byte). So the upper two hex digits are always 0.
// replace auto-interrupts 1 and 5 so that they don't interfere with _rowread()
SetIntVec(AUTO_INT_1,DUMMY_HANDLER);
SetIntVec(AUTO_INT_5,DUMMY_HANDLER);
This is how we redirect the AMS interrupt handlers to nothing.
SetIntVec() (Set interrupt vector) performs this function for us. We
want to redirect auto-interrupts 1 and 5 to use the code from
DUMMY_HANDLER. The DUMMY_HANDLER is simply a do-nothing block of code
defined in TIGCC. It just makes it easy for us to use low-level keyboard
reading. We might cover custom interrupt handlers in a furute lesson.
// draw the block on the screen at our initial position
drawBlock(pos);
The next segment in our code is to draw our sprite at it's initial
location. Remember that we initialized the POSITION structure pos with
the (x,y) position (0,0). So, our drawBlock() function (which just draws
our block sprite) is given the position structure so it can see where to
draw our block sprite at.
// until the user presses ESC
while (!quit(calc)) {
The main segment of our program focuses in the body of this while loop.
Since the keyboard matrix is different on the TI-89 than the
TI-92+/V200, to check for the ESC key, we need to do different
operations. So, we will put the necessary options in a function called
quit(). Then if quit() returns true, it means ESC was pressed. So let's
take a look at the quit() function:
short int quit(short int calc) {
if (calc == 0) { // test for TI-89 ESC key
if (_rowread(TI89_ESCROW) & TI89_ESCKEY) {
return 1;
}
} else { // test for TI-92+ ESC key
if (_rowread(TI92_ESCROW) & TI92_ESCKEY) {
return 1;
}
}
return 0;
}
The quit() function is pretty simple, and it demonstrates the first use
of the _rowread() function. So, if our calc variable equals 0, which
means we are using a TI-89, then we do the bitwise AND on the
_rowread(TI89_ESCROW) and the TI89_ESCKEY. These constants were defined
at the top of the file.
#define TI89_ESCROW 0xFFBF // the ESC row on the TI-89 keyboard matrix
#define TI89_ESCKEY 0x0001 // the ESC key inside that row
#define TI92_ESCROW 0xFEFF // the ESC row on the TI-92+ keyboard matrix
#define TI92_ESCKEY 0x0040 // the ESC key inside that row
Remember from above that the _rowread() function needs an inverse bit
mask so it knows which row of the keyboard matrix to return. So, we use
1's for all the rows we want to mask, instead of using 0's which is how
normal masks are done. The row on the TI-89 keyboard matrix we need for
the ESC key is the 7th row, which corresponds to the 6th bit. So, to get
the proper mask, we subtract 0x0040 (bit 6) from the inverse mask
(0xFFFF) and we end up with 0xFFBF, which is the proper inverse mask
setting to read the ESC row on the keyboard for the TI-89. Now, inside
this row, the ESC key is the 0 bit, so we use the normal mask 0x0001 for
the 0 bit. The TI-92+/V200's ESC row is the 10th row, and since the 10th
bit (0b0000 0001 0000 0000) is 0x0100, we subtract 0xFFFF by 0x0100 and
get the correct inverse mask for the TI-92+/V200 ESC ROW, 0xFEFF. This
becomes easier with practice, so be patient if it doesn't make perfect
sense right now.
Now, we know the _rowread() function returns the keyboard row. So since
we don't need to test for any other keys in this row, we can just
bitwise AND the result of the _rowread() function and the proper ESCKEY
value for the calculator (either TI89_ESCKEY or TI92_ESCKEY). If the
result of the if-condition is true, then we return 1, meaning, yes, exit
this program. Simple, no? Well, if not, it will get easier with
practice.
key = _rowread(ARROW_ROW);
Inside the while loop, we start the keyboard reading process. Since we
will be testing 4 values from the same row (all the arrow keys are
defined on the same row of the keyboard matrix on both calculators), we
can just save the result of the row read so we can test for different
keys in the same row.
// check for UP arrow
if (key & keys[UP]) {
move(&pos,UP,(const SCREEN)screen);
}
// check for LEFT arrow
if (key & keys[LEFT]) {
move(&pos,LEFT,(const SCREEN)screen);
}
// check for DOWN arrow
if (key & keys[DOWN]) {
move(&pos,DOWN,(const SCREEN)screen);
}
// check for RIGHT arrow
if (key & keys[RIGHT]) {
move(&pos,RIGHT,(const SCREEN)screen);
}
Okay, the next thing to do after reading the keyboard row is to check
the row against the arrow keys. The process is the same as before, but
now we have to bitwise AND the key (our keyboard row variable) and the
keys[] array of our arrow key masks (remember that although the keyboard
row for the arrow keys is the same on both 89 and 92+/V200 keyboard
matricies, the positions on the row are not the same, which is why we
created the keys[] array in the first place). So, once we do the bitwise
AND, if the operation returns true, then that key is being held down.
Remember that we shouldn't test for equality with the key mask, because
they might be holding down more than one arrow. So, if we want to check
that, we need to only test one arrow at a time.
So, assuming one of the AND's returns true, then we call the move()
function, which determines if we can move in the direction specified,
and if so, then moves the block.
void move(POSITION *newPosition, short int direction, const SCREEN screen) {
POSITION oldPosition = *newPosition;
switch (direction) {
case UP:
// if we can move up, then do so
if ((newPosition->y - MOVE_RATE) > 0) {
newPosition->y -= MOVE_RATE;
moveBlock(oldPosition,*newPosition);
}
break;
case DOWN:
// if we can move down, then do so
if ((newPosition->y + MOVE_RATE) < (screen.height - MOVE_RATE)) {
newPosition->y += MOVE_RATE;
moveBlock(oldPosition,*newPosition);
}
break;
case LEFT:
// if we can move left, then do so
if ((newPosition->x - MOVE_RATE) > 0) {
newPosition->x -= MOVE_RATE;
moveBlock(oldPosition,*newPosition);
}
break;
case RIGHT:
// if we can move right, then do so
if ((newPosition->x + MOVE_RATE) < (screen.width - MOVE_RATE)) {
newPosition->x += MOVE_RATE;
moveBlock(oldPosition,*newPosition);
}
break;
}
}
Our biggest function exists so we can move the sprite around the screen.
First of course, we need to check that the sprite will be displayed at a
valid location if we move it. So, we make sure the direction will not be
displayed off the screen and call the moveBlock() function to erase the
old block and draw the new one. If you need more on this, consult
lessons 3, 4 and 7 for sprites, functions, and structures, respectively.
The function should be fairly easy for you in this stage of your C
programming career.
// slow the program down
delay();
The last part of the loop is our delay function. One of the advantages
(and at times problems) with disabling the auto-interrupts is that our
programs gain a huge speed advantage. So much so in fact that our
program will run much too fast. Our sprite would move all the way across
the screen with one key press. The delay function "solves" this problem
by creating busy work to simulate the time it took for the
auto-interrupts to work.
// slow the program down
void delay(void) {
short int loop = 1800, randNum;
// generate random numbers to slow down the program...
while (loop-- > 0) {
randNum = rand() % loop;
}
}
The delay function is very unique. It's only purpose is to slow down the
program. To do this, we need to create some "busy work" for the
calculator to do. You might think we could just make an empty loop, and
I have done this in the past. However, more recent versions of TIGCC
have a smarter compiler which figured out I wasn't really doing anything
and eliminated my code. It was supposed to optimize the program for
speed, but of course, if we are trying to slow the program down, we
don't want to optimize for speed.
So, to slow the program down, we generate 1800 random numbers. This
slows the game down by a few thousandths of a second. It's not a perfect
delay. If you try to press the arrow keys once, it sometimes registers
as two presses, but this is a pretty good delay. Most games use smooth
motion anyway, so the double-reading of an arrow key is not usually much
of an issue, but if it were, we would need to play around with the delay
and test the program. If you put the delay too high, it loses
keystrokes, so we don't want to do that, but if we set it too low, then
we register multiple keystrokes. It would only be necessary to tweak
it further than this if we needed the keyboard reading to be perfect.
In reality, this is a terrible way of doing this, but it will suit our
example purposes fine.
// restore auto interrupt 1
SetIntVec(AUTO_INT_1,interrupt1);
Remember that we redirected auto-interrupts 1 and 5, so we need to
restore them before we exit the program. If we do not, the calculator
will stop working. Above we said that if you needed to use a AMS input
routine for some reason (dialog boxes being the most common), you would
need to re-enable this interrupt before you could do that. This is how
we do that. So, if we wanted to use a dialog box somewhere, we could do
this:
SetIntVec(AUTO_INT_1,interrupt1);
DlgMessage("Pop-Up","Surprise Message!",BT_OK,BT_NONE);
SetIntVec(AUTO_INT_1,DUMMY_HANDLER);
I'm pretty sure you don't need interrupt 5 for dialog boxes, but not
100%. I haven't tested this. Don't forget to disable it again after you
do the function you need to do, otherwise our low level keyboard reading
will mess up.
Well, that's a pretty good size program for just an example, but it's a
very important step into game programming, if you want to do that. But
more importantly, it illustrates the concept of bitwise operations
through the use of AND. We will talk about the other bitwise operations
soon too.