PC character ROM revealed. (BANNER.C program) (Column)
by Tom Campbell
This month's program, BANNER.C, digs deep into the guts of your PC by using the character ROM to display letters eight times their normal size in text mode. Is shows both an underused PC resource (the system font data) and some useful tricks of the C trade: bit-shifting operators, reading bit values, using array notation for pointers, and reading absolute addresses on the PC. (You can find the source code for BANNER.C on COMPUTE/NET on GEnie or America Online.)
Banner asks you for a string of up to ten characters and a fill character. The fill character is what makes up the dots in the letter--for example, the asterisk character or one of the extended ASCII box characters. You can enter the extended ASCII characters by holding down the Alt key and pressing up to three digits, creating a number between 0 and 255. I suggest 177, 178, 219, 248, and 254. Then Banner displays the string on the center row of your screen.
Your PC video card has an 8 x 8 character set in it ROM, even though, in the case of EGAs and better, that's not the default system font. But it's there all the time to provide compatibility with earlier adapters. The dots that make up the 8 x 8 font are found at location F000:FA6E hex and are laid out in 256 sets of eight bytes each, with the first byte making up the top row of dots in each letter, the second byte making up the second row of dots, and so on. The example below shows the layout of the letter A.
The first byte, numbered 0, contains the hex value 3Eh. This makes little sense until you view it as the binary value 01111100. Replace each of the 1s with an X, and you have the top line of the A. The next byte is hex C6h, or binary 11000110. You may notice that bit 0 is unoccupied for almost every character, as is byte 7. Bit 0 forms the space between letters--it's just an empty column formed by leaving bit 0 of each of the rows of bytes cleared to 0. An exception is the underline character, which, appropriately, goes all the way across the character matrix. And byte 7 is the space left for descenders--the tails for g, j, p, q and y.
The chart above isn't quite accurate. The byte numbered 0 is actually byte 520. Remember that the uppercase A is ASCII 65. There are eight bytes of data per character. Multiply 65 by 8, and you get 520. Uppercase B is at 528, and C is 536. The formula, then, as it appears in the WriteChar() routine, is CharacterRom = CharacterRom + Letter * 8.
CharacterRom was previously initialized and allocated at the same time--another handy C trick: char far *CharacterRom = (void *) 0xF000FA6E. This is identical to char far *CharacterRom; ... CharacterRom = (void *) 0xF000FA6E.
The previous example is clearer in this case, because we want to emphasize the nature of CharacterRom as an absolute address. The easy part is explaining that 0xF000FA6E is hex notation for the absolute address whose segment: offset value is, as explained earlier, F000:FA6E. That's where the 1024 bytes of character ROM data appear. Let's dissect the rest of the line piece by piece, because much of it isn't what it seems. The char means that we will treat the address as a pointer to a character. Since a character is guaranteed by the ANSI standard to be the same size as a byte and since C lets us use array notation wherever we see a pointer, later we'll be able to calculate the location of each row of character data like this, even though CharacterRom wasn't declared as an array: Bitmap = CharacterRom[EachRow].
Let's use the top row of A, binary 01111100, as an example. The Binary numbers mirror the physical layout of bits in memory, so this loop starts at the left bit and moves right: for (EachCol=7; EachCol>=0; EachCol--).
The line "if (Bitmap & 1)" is another good example of C's terseness at work, taking advantage of C's treatment of an if expression as an integer result. In Pascal, you'd use this code: IF (Bitmap AND 1) = 1 THEN.
Using the bitwise AND operator of C to see whether a bit is set makes it easier to see what's going on. If the right-most bit of Bitmap is indeed set to 1, the cursor is positioned appropriately, and the FillChar is written directly to screen memory. If bit 0 is clear (that is, with a value of 0 instead of 1), nothing happens. Finally, the byte is pushed one bit to the right, moving bit 1 into the 0 position, 2 into the 1 position, and so on, up to bit 7.
With only a little extra work, you could beef up WriteChar() to center the text both vertically and horizontally.