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

     Introduction to C

     Keyboard Input

     Graphics Intro

     Slider Puzzle 1

     Functions

     Pointers

     Dynamic Memory

     Slider Puzzle 2

     Structures

     Bit Manipulation

       Part I

       Part II

       Part III

     Advanced Pointers

     File I/O

     Graduate Review

   Assembly

   Downloads

 Miscellaneous
   Links

   Cool Graphs

   Feedback Form

C Programming Lessons

TIGCC Programming Lessons

Lesson 8: Bit Manipulation

Step 4 - The First of Many Important Uses for Bitwise Operations

Start TIGCC and create a new project. Create a new C Source File named rowread. Modify the rowread file so that it looks like this:

rowread.c


#include <tigcclib.h>

enum ArrowKeys {UP,DOWN,LEFT,RIGHT};

#define MOVE_RATE   5   // move sprite 5 pixels at a time
#define HEIGHT      5   // sprites are 5 pixels tall

#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

#define ARROW_ROW   0xFFFE  // arrow row is the same on both calculators

typedef struct {
    unsigned char x, y;
} POSITION;

typedef struct {
    unsigned char width, height;
} SCREEN;

static unsigned char block[] = {0xFF,0xFF,0xFF,0xFF,0xFF};

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
    }
}

inline void drawBlock(POSITION pos) {
    Sprite8(pos.x,pos.y,HEIGHT,block,LCD_MEM,SPRT_XOR);
}

// the name looks better, but with unmasked sprites, drawing and erasing are the same
inline void eraseBlock(POSITION pos) {
    drawBlock(pos);
}

// I think you can see why these are all declared 'inline'
inline void moveBlock(POSITION oldPosition, POSITION newPosition) {
    eraseBlock(oldPosition);
    drawBlock(newPosition);
}

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;
}

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;
    }
}

// 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;
    }
}

void _main(void) {
    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;

    // seed the random number generator
    randomize();

    // initialize the screen dimensions
    screen.width = LCD_WIDTH;
    screen.height = LCD_HEIGHT;

    // 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);

    // 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);

    // clear the screen
    ClrScr();

    // draw the block on the screen at our initial position
    drawBlock(pos);

    // until the user presses ESC
    while (!quit(calc)) {
        key = _rowread(ARROW_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);
        }

        // slow the program down
        delay();
    }

    // restore auto-interrupts
    SetIntVec(AUTO_INT_1,interrupt1);
    SetIntVec(AUTO_INT_5,interrupt5);

    // wait for input before exiting the program
    ngetchx();
}

Step 4a - Compile and Run the Program

Save the project and build it. Send the program to TiEmu and run it. It will look something like this, though I couldn't make very good screenshots for a sprite which moves so fast.

TI-89 AMS 2.05 rowread.89z TI-92+ AMS 2.05 rowread.9xz

Step 4b - Program Analysis

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.

Continue in Part III

 

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

Get Firefox!    Valid HTML 4.01!    Made with jEdit