home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Programming Tool Box
/
SIMS_2.iso
/
bp_6_93
/
bonus
/
winer
/
chap10.txt
< prev
next >
Wrap
Text File
|
1994-09-03
|
52KB
|
1,190 lines
CHAPTER 10
KEY MEMORY AREAS IN THE PC
Two very important BASIC keywords that are sadly neglected by many
programmers are PEEK and POKE. Most people understand that these let you
read from and write to memory locations. But what are they really good
for? The whole point of a high-level language like BASIC is to avoid such
direct memory access, and to many programmers these commands may seem like
an enigma.
In most cases, you *don't* need to access memory with PEEK and POKE.
Unlike C and assembly language that require direct memory operations to
process strings and arrays, BASIC includes a full complement of commands
for this. However, there is at least one important use for PEEK and POKE
that cannot be accomplished in any other way: accessing low memory.
The portion of memory in every PC that begins at Hex address 0000:0400
is called the *BIOS Data Area*, and it contains much useful information.
For example, the equipment word at address &H410 tells how many diskette
drives are installed, and how many parallel and serial ports there are.
The keyboard status flags at address &H417 can be read (and written), to
reflect whether the Caps Lock and NumLock states are active.
In this chapter I will describe all of the low memory locations that are
relevant to a BASIC program, and present numerous practical examples to
show how this data can be utilized. This is by no means a complete list
of every BIOS data address that is available in the PC. Rather, I have
purposely limited it to those that I have found useful.
IMPROVING PEEK AND POKE
=======================
One potential limitation that needs to be addressed first is how to access
full words of data. BASIC's PEEK and POKE operate on single bytes only,
and reading or writing two bytes at a time is a messy proposition at best.
Chapter 9 introduced a pair of routines called PeekWord and PokeWord,
that allowed accessing memory a word at a time. In the context those were
presented, a fair amount of code could be saved by consolidating the
necessary code into a subprogram or function. But in the interest of speed
and even further code size reductions, the following assembly language
routines are better still.
;PEEKPOKE.ASM, simplifies access to full words
.Model Medium, Basic
.Code
PeekWord Proc Uses ES, SegAddr:DWord
Les BX,SegAddr ;load the segment and address
Mov AX,ES:[BX] ;read the word into AX
Ret ;return to BASIC
PeekWord Endp
PokeWord Proc Uses ES, SegAddr:DWord, Value:Word
Les BX,SegAddr ;load the segment and address
Mov AX,Value ;and the new value to store there
Mov ES:[BX],AX ;write the value into memory
Ret ;return to BASIC
PokeWord Endp
End
Both of these routines expect the parameters to be passed by value, for
faster speed and smaller code. Therefore, you will declare them as
follows:
DECLARE FUNCTION PeekWord%(BYVAL Segment%, BYVAL Address%)
DECLARE SUB PokeWord(BYVAL Segment%, BYVAL Address%, BYVAL Value%)
Then to read a word of memory--say, the address of the LPT1 printer adapter
at address &H408--PeekWord would be invoked like this:
LPT1Addr% = PeekWord%(0, &H408)
And to write the letter "A" in the lower left corner of a color display
screen in white on blue you could use PokeWord, thus:
CALL PokeWord(&HB800, 3998, &H1741)
Notice that PeekWord returns a negative value for numbers greater than
32767. This is normal, as explained in Chapter 2. However, the same
negative value that PeekWord returns can be used as an argument to PokeWord
with the correct results.
LOW MEMORY ADDRESSES
====================
The sections that follow are organized by category, since this is how low
memory is arranged in the PC. That is, one section discusses the RS-232
communications data area, the next shows the portion of memory used by the
printer adapters, and so forth. Each address is listed in ascending order;
by convention, Hex notation is used exclusively for these addresses. In
all of the examples shown here, you will use a segment value of zero.
It is important to understand that besides memory addresses that are
accessed with PEEK and POKE (or in this case their full-word equivalents),
the IBM PC family also has a series of input and output ports. These ports
are accessed using INP and OUT commands instead of PEEK and POKE. I
mention this here because ports are referred to in several places in the
discussions that follow. In particular, the communications ports that are
exchanged in the next section are in fact port numbers, and not memory
addresses. Some useful port numbers are given at the end of this chapter,
along with code examples that show how to read from and write to them.
Table 10-1 provides a summary of all the low memory addresses that are
described in this chapter.
Address Meaning
======= ==========================================
&H400 2 bytes, COM1 port number
&H402 2 bytes, COM2 port number
&H404 2 bytes, COM3 port number
&H406 2 bytes, COM4 port number
&H408 2 bytes, LPT1 port number
&H40A 2 bytes, LPT2 port number
&H40C 2 bytes, LPT3 port number
&H40E 2 bytes, LPT4 port number
&H410 2 bytes, Equipment List
&H413 2 bytes, installed memory (K)
&H417 2 bytes, keyboard status
&H418 2 bytes, enhanced keyboard status
&H41A 2 bytes, keyboard buffer head pointer
&H41C 2 bytes, keyboard buffer tail pointer
&H41E 30 bytes, keyboard buffer
&H43F 1 byte, diskette motor on indicator
&H440 1 byte, diskette motor countdown timer
&H449 1 byte, current video mode
&H44A 2 bytes, current screen width (columns)
&H44C 2 bytes, current video page size (bytes)
&H462 1 byte, current video page number
&H463 2 bytes, CRT controller port number
&H46C 4 bytes, long integer system timer count
&H478 4 bytes, LPT1 - LPT4 timeout values
&H484 1 byte, EGA/VGA screen height (rows)
&H485 2 bytes, character height (scan lines)
&H487 1 byte, EGA/VGA Features bits
&H4F0 16 bytes, Inter-Application Area
&H500 1 byte, PrtSc busy flag
&H504 1 byte, active drive for one-diskette PC
Table 10-1: Key low memory addresses in the PC.
COMMUNICATIONS PORT ADDRESSES
=============================
The four words starting at address &H400 hold the port numbers for each
installed RS-232 communications adapter. For example, the port number for
COM1 is contained in the word at address &H400, and the port number for
COM3 is at address &H404. Because these port numbers are words rather than
bytes, the COM1 port number is contained in both &H400 and &H401. Thus,
COM2 starts at address &H402, and COM3 starts at &H404.
BASIC allows you to open only COM ports 1 and 2; however by exchanging
these addresses you can substitute ports 3 and 4 if necessary. The
complete program that follows first swaps the port numbers for COM1 and
COM3, and then opens COM1 for output. Since the port numbers are swapped,
it is actually COM3 that is being opened.
DEFINT A-Z
DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
DECLARE SUB PokeWord (BYVAL Segment, BYVAL Address, BYVAL Value)
COM1 = PeekWord%(0, &H400) 'save COM1 port number
COM3 = PeekWord%(0, &H404) 'save COM3 port number
CALL PokeWord(0, &H400, COM3) 'assign COM3 to COM1
CALL PokeWord(0, &H404, COM1) 'and then COM1 to COM3
OPEN "COM1:1200,N,8,1,RS,DS" FOR RANDOM AS #1
PRINT #1, "ATDT 1-555-1212" 'dial information
CLOSE #1
CALL PokeWord(0, &H400, COM1) 'restore the original values
CALL PokeWord(0, &H404, COM3)
PRINTER PORT ADDRESSES
======================
The four printer port numbers start at address &H408, and they are similar
to those used to hold the communications ports and may also be exchanged
if necessary. For example, if you have a program that uses LPRINT
commands, all printed output will be sent to LPT1. If at some later time
you want to use the same program with LPT2, you can exchange the port
numbers instead of having to rewrite the program. A short code fragment
that does this is shown following.
DEFINT A-Z
DECLARE FUNCTION PeekWord% (BYVAL Segment, BYVAL Address)
DECLARE SUB PokeWord (BYVAL Segment, BYVAL Address, BYVAL Value)
LPT1 = PeekWord%(0, &H408) 'save LPT1 port number
LPT2 = PeekWord%(0, &H40A) 'save LPT2 port number
CALL PokeWord(0, &H408, LPT2) 'assign LPT2 to LPT1
CALL PokeWord(0, &H40A, LPT1) 'and LPT1 to LPT2
LPRINT "This is printed on LPT2"
CALL PokeWord(0, &H408, LPT1) 'restore the original values
CALL PokeWord(0, &H40A, LPT2)
LPRINT "And now we're back to LPT1" 'prove it worked
Like the communications port addresses, each printer port address is a
full word, so while the first is located at address &H408, the second is
at &H40A. You will also find PeekWord useful because it does not require
you to change the current DEF SEG setting. Although there is no harm in
assigning a new DEF SEG value in most cases, it is not easy to restore it
to the original setting. Therefore, when writing reusable subprograms and
functions that need to access memory, you don't have to worry about
affecting a subsequent PEEK or BLOAD in the main program.
SYSTEM DATA
===========
One of the most valuable data items in low memory is the equipment list
in the word starting at address &H410. The information contained here is
bit coded, to indicate which and how many peripherals are installed in the
host PC. Figure 10-1 shows the organization of this word. Bits not
identified are either reserved, or not particularly useful.
15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 <- bit numbers
x x -------------------------------------------- printers
x -------------------------------------- 1 = game port
x x x -------------------------- serial ports
x x -------------------- diskette *
x x -------------- video mode
x ------ coprocessor
x -- diskette *
* If Bit 0 is set, then bits 6 and 7 together reflect the number of
diskette drives *less one*. If Bit 0 is clear then no diskette drives are
installed.
Figure 10-1: The organization of the equipment list word at address &H410.
Because the data in this word is bit coded, you must use AND to extract
the necessary information. For example, to see if a math coprocessor is
installed you must turn off all but bit 1, and see if the result is zero
or not:
IF PeekWord%(0, &H410) AND 2 THEN
PRINT "A coprocessor is installed."
ELSE
PRINT "Sorry, no coprocessor detected."
END IF
This brings up an important point, because it is not immediately obvious
what values you should use to isolate the various bits in a word. It would
be terrific if Microsoft BASIC offered the ability to handle binary values
directly. The Microsoft Macro Assembler allows this, as does PowerBasic.
In the absence of &B and a BIN$ function, the following short function can
be used to determine the correct integer value for a given sequence of
binary bits.
FUNCTION Bin% (Bit$) STATIC
Temp& = 0
Length = LEN(Bit$)
FOR X = 1 TO Length
IF MID$(Bit$, Length - X + 1, 1) = "1" THEN
Temp& = Temp& + 2 ^ (X - 1)
END IF
NEXT
IF Temp& > 32767 THEN
Bin% = Temp& - 65536
ELSE
Bin% = Temp&
END IF
END FUNCTION
Given a string of binary digits of the form "01011001", the Bin function
returns an equivalent integer value. You could add this function to your
programs, or use it to determine constant values ahead of time. For
example, to determine the number of diskette drives that are installed
requires isolating bits 6 and 7. This is simple in assembly language,
where you can specify an AND mask using 11000000b as a value. The example
below obtains the equipment word, and then uses the Bin function to disable
all but bits 6 and 7.
Equipment = PeekWord%(0, &H410)
Floppies = 1 + (Equipment AND Bin%("11000000")) \ 64
PRINT Floppies; "diskette drive(s) installed"
Although the Bin function is used in the code, I recommend that you create
a simple test program first, to determine the value of 11000000 (192) once
ahead of time. Then, the Bin function can be omitted from the final
program and the second line would be changed as follows:
Floppies = 1 + (Equipment AND 192) \ 64
Notice the use of parentheses to force BASIC to combine Equipment and the
number 192 before dividing by 64 with AND. If these are omitted BASIC
will instead combine Equipment with the result of 192 divided by 64, which
is not correct.
One final technique you should understand is how to shift bits into the
correct position to obtain the actual value the bits represent. Treated
as bits alone, the number of diskette drives is represented as 00, 01, 10,
or 11, and the decimal equivalents for these binary numbers are 0, 1, 2,
and 3. But because of their positioning in the equipment word, the bits
must be shifted to the right six places. After all, the value 11000000
(192) is certainly not the same as the value 11 (3).
This is handled simply and elegantly using integer division as shown.
To shift a number right one position divide it by 2; to shift right 2
places divide by 4, and so forth. Since the diskette bits need to be
shifted six places, the equipment variable is divided by 64 after AND is
used to mask off the unrelated bits. Likewise, to shift bits left you can
multiply by 2, 4, 8, and so forth. The number to use when dividing or
multiplying can also be determined by raising 2 to the number of bits
power. For example, to shift a number right five places you would divide
by 2 ^ 5 = 32.
A problem arises when dealing with the highest order bit, because to
BASIC this bit implies a negative number. Therefore, when bit 15 is set,
dividing will not produce the expected results. One workaround that is
admittedly clumsy is to test that bit explicitly, then mask it off and
shift the bits as needed, and finally use an IF test to see if the bit had
been set. The only place this is necessary in the equipment list is when
reading the number of parallel printers that are present. The first
example below reports the number of serial ports, and the second tells how
many parallel ports are installed.
Equipment = PeekWord%(0, &H410)
Serial = (Equipment AND Bin%("11000000000")) \ 512
PRINT Serial; "serial port(s) installed"
IF Equipment AND Bin%("1000000000000000") THEN
HiBitSet = -1
END IF
Parallel = (Equipment AND Bin%("0100000000000000")) \ 16384
IF HiBitSet THEN Parallel = Parallel + 2
PRINT Parallel; "parallel port(s) installed"
In the interest of completeness I should point out that it is not strictly
necessary to manipulate bit 15 when accessing the equipment word. Since
none of the information straddles a byte boundary, BASIC's PEEK can in fact
be used to read just the high byte. Since a byte value is never higher
than 255, the entire issue of saving and then masking that bit can be
avoided. But there are other situations you may encounter where an entire
word must be processed and the highest bit may be set.
The final useful item in the equipment word is the initial video mode.
I've seen many programmers read use information to determine if a color
or monochrome monitor is installed like this:
DEF SEG = 0
IF (PEEK(&H410) AND &H30) = &H30 THEN
' monochrome
ELSE
' color
END IF
There are two problems with this approach. The most serious is that this
reflects the monitor that was active when the PC was first powered up.
These days, many people have two monitors connected to their PC, and you
usually need to know which is currently active. The other problem is this
requires more code than the better method I showed in Chapter 6 which reads
the port address of the currently active video adapter:
DEF SEG = 0
IF PEEK(&H463) = &HB4 THEN
' monochrome
ELSE
' color
END IF
Besides the equipment word at address &H410, another word at address &H413
holds the amount of memory that is installed in KiloBytes. Note that this
word does not reflect any extended or expanded memory that may be present.
Also note that a much better indicator of how much memory is actually
available to a program is BASIC's FRE(-1) function. The short code
fragment below shows how to determine the total DOS-accessible memory that
is installed.
TotalK = PeekWord%(0, &H413)
PRINT TotalK; "K Bytes present in this PC."
KEYBOARD DATA
=============
As with the equipment word, the keyboard data area also maintains bit-coded
information. However, this word indicates the setting of the various
keyboard shift states. Unlike many of the other addresses in the BIOS data
area, some of these bits may be written to as well as read from.
The byte at address &H417 shows the current status of all of the shift
keys, and the lower four bits may be either read or written. The remaining
bits in this byte should not be written to, nor should you alter any of the
bits in the next byte at address &H418. Figure 10-2 shows the meaning of
each bit in the byte at address &H417, and Figure 10-3 shows the bits at
address &H418 that relate to extended keyboards only.
7 6 5 4 3 2 1 0 <-- bits
x --------------------------------- Insert state
x ----------------------------- Caps Lock
x ------------------------- Num Lock
x --------------------- Scroll Lock
x ----------------- Alt key
x ------------- Ctrl key
x --------- Left Shift key
x ----- Right Shift key
Figure 10-2: The organization of the keyboard data byte at address &H417.
7 6 5 4 3 2 1 0 <-- bits
x --------------------------------- Insert
x ----------------------------- Caps Lock
x ------------------------- Num Lock
x --------------------- Scroll Lock
x ----------------- Pause state
x ------------- Sys Req
x --------- Left Alt key
x ----- Left Ctrl key
Figure 10-3: The organization of the extended keyboard data byte at address
&H418.
The various flags in the upper four bits at address &H417 are toggled on
and off by the BIOS each time the corresponding keys are pressed. For
example, bit 6 is set while the Caps Lock is active, and bit 5 is clear
when Num Lock is not in effect. Note, however, that the Insert flag is of
no practical use, and you should not rely on that bit in your programs.
If you are writing an input routine (or using the one shown in Chapter 6)
you should keep track of the insert status manually.
The lower four bits indicate the current state of the various shift
keys, and they are set only while the associated key is actually being
pressed. Bits in the next word at address &H418 let you determine which
Alt and Ctrl keys are pressed, for keyboards that have more than one of
those keys. In most cases you will probably just want to know if these
keys are active, and not distinguish between the left and the right key.
Therefore, you will usually ignore the extended keyboard information,
unless you need to detect the SysReq key.
As with the equipment list, you will use a combination of PeekWord (or
PEEK) to read all of the flags, and then use AND to isolate just those bits
you care about. Because there is only one bit that corresponds to each
keyboard state flag, it is not necessary to divide or multiply to convert
multiple bits into a number.
The examples below show how to test each of the bits in the byte at
address &H417, without regard to the extra Ctrl and Alt key information
contained at address &H418.
CLS
PRINT "Press the various Shift and Lock keys, ";
PRINT "then press Escape to end this madness."
COLOR 0, 7
DO
Status = PeekWord%(0, &H417)
LOCATE 10, 1
IF Status AND 1 THEN
PRINT "RightShift"
ELSE
GOSUB ClearIt
END IF
LOCATE 10, 11
IF Status AND 2 THEN
PRINT "Left Shift"
ELSE
GOSUB ClearIt
END IF
LOCATE 10, 21
IF Status AND 4 THEN
PRINT "Ctrl key"
ELSE
GOSUB ClearIt
END IF
LOCATE 10, 31
IF Status AND 8 THEN
PRINT "Alt key"
ELSE
GOSUB ClearIt
END IF
LOCATE 10, 41
IF Status AND 16 THEN
PRINT "ScrollLock"
ELSE
GOSUB ClearIt
END IF
LOCATE 10, 51
IF Status AND 32 THEN
PRINT "Num Lock"
ELSE
GOSUB ClearIt
END IF
LOCATE 10, 61
IF Status AND 64 THEN
PRINT "Caps Lock"
ELSE
GOSUB ClearIt
END IF
LOCATE 10, 71
IF Status AND 128 THEN
PRINT "Insert"
ELSE
GOSUB ClearIt
END IF
LOOP UNTIL INKEY$ = CHR$(27)
COLOR 7, 0
END
ClearIt:
COLOR 7, 0
PRINT SPACE$(10);
COLOR 0, 7
RETURN
As you can see, to read a single bit you use AND to isolate it from the
rest, and then test if the result is non-zero. Setting a bit requires
slightly more work, because it is important not to disturb the other bits
in that byte. This requires that you first read the current information,
change only the bit or bits of interest, and then write the modified data
back to the same location. The next short example shows how to turn the
CapsLock state on and then off again.
CurStatus = PeekWord%(0, &H417)
NewStatus = CurStatus OR Bin%("1000000")
CALL PokeWord(0, &H417, NewStatus)
PRINT "Press a key to turn off CapsLock"
WHILE INKEY$ = "": WEND
NewStatus = NewStatus AND Bin%("10111111")
CALL PokeWord(0, &H417, NewStatus)
Notice the difference between how OR is used in the first example, and how
AND is used in the second one. In the first case we want to set a bit,
so only that bit is specified in the binary mask. The remaining bits stay
the same as they were--if they are already set then OR will leave them that
way. But to turn off the CapsLock bit requires that all of the mask bits
be set *except* the one you wish to force off. Other bits that were
already on will remain on after being combined with AND and 1.
THE KEYBOARD BUFFER
The next group of low memory keyboard addresses relate to the keyboard
buffer. As you undoubtedly know, every PC has a keyboard buffer that can
hold up to fifteen keystrokes. When a program is off doing something and
is unable to read the keyboard, the BIOS keyboard routines will store keys
that have been typed. Then, when the program finally gets around to
reading the keyboard, they are waiting there to be read. The keyboard
buffer is therefore also called the *type-ahead* buffer.
A series of 34 bytes are set aside for the keyboard buffer. Two words
(four bytes) are used to hold the current head and tail pointers that show
where the next key will be read from, and where the next will be stored.
The current head address is stored at address &H41A and the tail at address
&H41C. Thirty additional bytes are used to store the actual keystrokes,
with two bytes used for each. The keyboard buffer is called a *circular
buffer*, because the start and end points are constantly revolving.
When a PC is first powered up, the head of the buffer holds the address
&H41E, which is the start of the buffer memory area. The tail is also
initially set to that same address, until a key is pressed. When that
happens, the tail pointer is advanced by 2, and the character and its scan
code are placed into the buffer. Each time a new key is pressed the
character and scan code are added to the end of the buffer and the tail
pointer is advanced by two; each time a key is read by an application the
word at the current head is returned and the head pointer is advanced.
Note that the head and tail addresses assume a segment of &H40, rather
than zero. Therefore, the actual values stored range from &H1E through
&H3A rather than &H41E through &H43A. Of course, address 0000:041E is the
same as address 0040:001E, and you can think of the buffer address either
way. I usually treat all of low memory as being located in segment 0,
because that can often save a byte of code. BASIC (or assembly language,
for that matter) can pass the number zero by value using only three bytes,
compared to the four bytes needed to pass any other number.
The program below shows how to determine the number of keys that are
currently pending in the buffer, and also which one will be returned next.
CLS
PRINT "You have two seconds to press a few keys..."
Pause! = TIMER
WHILE Pause! + 2 > TIMER: WEND
BufferHead = PeekWord%(0, &H41A)
BufferTail = PeekWord%(0, &H41C)
NumKeys = (BufferTail - BufferHead) \ 2
IF NumKeys < 0 THEN NumKeys = NumKeys + 16
PRINT "There are"; NumKeys; "keys pending in the buffer."
PRINT "The next key waiting to be read is ";
NextKey = PeekWord%(&H40, BufferHead)
IF NextKey AND &HFF THEN
PRINT CHR$(34); CHR$(NextKey AND &HFF); CHR$(34)
ELSE
PRINT "Extended key scan code"; NextKey \ 256
END IF
This program starts by waiting two seconds giving you a chance to press a
few keys. It then reads the buffer head and tail pointers, and from that
calculates the number of keys that are pending in the buffer. With a
circular buffer the head address may be higher the tail address, so a
separate test is needed to account for that.
Next, the word at the head of the buffer is retrieved, which indicates
the next available key. Since the head and tail pointers assume segment
&H40, I used that instead of segment 0. PeekWord%(0, &H41E) produces less
code than PeekWord%(&H40, &H1E); however, PeekWord%(0, &H400 + BufferHead)
is worse than PeekWord%(&H40, BufferHead) because of the addition needed.
Data in the keyboard buffer is always a full word, and it is up to you
to determine if it is a normal ASCII key or an extended key's scan code.
A normal key is indicated with a non-zero low byte, and the high byte then
holds the physical hardware scan code which can usually be ignored. If the
low byte instead holds a value of zero, it is an extended key and the scan
code in the high byte indicates which one. Therefore, the BASIC statement
NextKey AND &HFF masks the high byte, to test if the low byte is non-zero.
If the key is extended, then NextKey \ 256 returns the value in the high
byte. This is similar to the earlier examples that shifted bits to the
right by dividing. Unlike the earlier tests that examined only some of the
bits in the equipment flag, we are interested in all of the bits in the
upper byte. Dividing by 256 copies the upper byte to the lower byte, thus
discarding the lower byte entirely.
You should also refer back to the StuffBuffer program shown in Chapter
6, which accesses the keyboard buffer directly and inserts new keystrokes.
DISKETTE DATA
=============
There are several bytes in low memory that relate to the floppy and fixed
disks in your PC, but most of them are best left alone. One exception,
however, is the diskette drive motor timeout duration. Whenever a diskette
drive is accessed, DOS first turns on the motor, and then waits a second
or two until the motor has come up to speed. Once DOS is certain that the
disk speed is correct, reading and writing are allowed.
Because of the time it takes the diskette to become ready, DOS also
keeps the motor running for two more seconds after a read or write has been
completed. This way, if another request comes along within that time,
further delays can be avoided because the motor is already running. If you
know that the data your program is accessing is on a floppy disk and there
may be pauses in the reading or writing, you can force the motor to stay
on longer than the normal two seconds.
The byte at address &H440 controls the motor hold time, and its value
is decremented at every system timer tick [every 1/18th second]. When DOS
has finished accessing a diskette, it places a value into this memory
location. And when the value is decremented to zero the motor is turned
off. The current motor on/off state is reflected by the byte at address
&H43F. The program that follows shows how you can modify the timeout value
by poking a new, higher value into address &H440 immediately after a
command that accesses the disk.
PRINT "Place a diskette in drive A and press a key ";
WHILE INKEY$ = "": WEND
FILES "A:*.*" 'this starts the motor
DEF SEG = 0
POKE &H440, 91 'force drive motor on for five seconds
DO
LOCATE 10, 1, 0
PRINT PEEK(&H43F),
PRINT PEEK(&H440)
LOOP WHILE PEEK(&H440)
BEEP 'watch the diskette light go out when you hear the beep
The value you store at address &H440 is the number of timer ticks that are
to elapse before the motor is turned off. Since a new timer tick occurs
every 18.2 seconds, you will multiply the number of seconds times this
value using Value% = Seconds * 18.2.
DISPLAY ADAPTER DATA
====================
As with the diskette data area, a lot of information is available that
pertains to the video display, and most of it is of little use in an
application programming context. Therefore, I will discuss only some of
this data.
The byte at address &H449 holds the current video mode. Unfortunately,
there is no easy way to relate the information in this byte to the current
BASIC SCREEN setting. Table 10-2 shows all of the possible values that
might be present.
Video Mode Description
========== =========================================
0 40 by 25 16-color text
1 40 by 25 16-color text, with color burst
2 80 by 25 16-color text
3 80 by 25 16-color text, with color burst
4 320 by 200 pixels 4-color graphics
5 320 by 200 pixels 4-color
6 640 by 200 pixels 2-color
7 80 by 25 monochrome text
13 320 by 200 pixels 16-color graphics
14 640 by 200 pixels 16-color graphics
15 640 by 350 pixels monochrome EGA graphics
16 640 by 350 pixels 16-color graphics
17 640 by 480 pixels 2-color graphics
18 640 by 480 pixels 16-color graphics
19 320 by 200 pixels 256-color graphics
Table 10-2: The video mode value at Hex address 0000:0449
Since you will always have set the video mode yourself with a SCREEN
statement, there is little reason to have to read the current mode
manually.
The word at address &H44A tells how many columns are on the display, and
the word at address &H44C holds the total size of the screen in bytes. In
a normal 80 column by 25 line screen mode, the value at address &H44C will
be 4096, even though the screen can hold only 4000 characters.
The byte at address &H462 holds the current video page number, starting
at page 0. Please understand that BASIC lets you set pages individually
for writing to and displaying, and the page reported here is that which is
visible on the monitor.
We have already looked at the data at address &H463, which holds the CRT
controller port address. Although this address is a full word, only the
lower byte needs to be examined to know the type of display that is active.
If the byte value at address &H463 is &HB4, then a monochrome monitor is
connected and being used. If a color adapter is active the value at this
byte will instead be &HD4.
SYSTEM TIMER DATA
=================
Every 18th second the BIOS timer generates an interrupt that increments
the master system timer count at address &H46C. This counter is stored as
a four-byte long integer; the count is initialized to zero at midnight, and
increases to a value of just over one 1.5 million at 11:59:59 pm.
In some cases using the BIOS timer count directly can help to reduce the
size of your programs, because BASIC's TIMER requires floating point math.
Chapter 9 discussed some of the issue involved in benchmarking a program,
and the examples there used TIMER to know when a new 1/18th second period
has just started and how long a sequence of commands took. The following
short program times a long integer assignment within a FOR/NEXT loop, and
it uses the PeekWord function to access the BIOS timer count directly.
Synch = PeekWord%(0, &H46C)
DO
Start = PeekWord%(0, &H46C)
LOOP WHILE Synch = Start
FOR X& = 1 TO 70000
Y& = X&
NEXT
Done = PeekWord%(0, &H46C)
PRINT Done - Start; "timer ticks have elapsed"
Note that it is possible for this program to report an incorrect elapsed
time, since it considers only the lower of the two timer words. If the
count exceeded 65,535 during the course of the timing, the lower word will
have wrapped around to a value of zero. An enhancement to this technique
would therefore be to create a PeekLong% function that returns the entire
four bytes in one operation. You could write such a function in assembly
language, or use BASIC like this:
FUNCTION PeekLong& (Segment%, Address%) STATIC
PeekLong& = PeekWord%(Segment%, Address%) + 65536 * _
PeekWord%(Segment%, Address% + 2)
END FUNCTION
Here, the PeekWord function is used to do most of the work, and the two
words are combined into a single long integer. When many timing operations
are needed using these functions can increase the speed of your programs,
as well as help to avoid the inclusion of the floating point math library
routines.
PRINTER TIMEOUT DATA
====================
Whenever data is sent to a parallel printer it is routed through a BIOS
service that handles the actual communications with the printer hardware.
If the printer is turned off or disconnected, the BIOS can detect that
immediately, and report the error to the calling program. But when the
printer is turned on but deselected (off-line) or if it has run out of
paper, the BIOS waits for a certain period of time before returning with
an error condition. This gives the operator a chance to fix the problem.
The amount of time the BIOS waits varies from PC to PC, and even between
different models of the same brand. The original IBM PC waited for only
a very short time, and would occasionally report an error incorrectly when
used with very slow printers. Modern PCs wait as long as two minutes
before timing out, which is more than enough time to reload a new ream of
paper. Unfortunately, if you want to test if a printer is ready before
using it, your program may appear to hang if the printer is disabled.
Although BASIC provides ON ERROR to trap for printer errors, many
programmers prefer to avoid ON ERROR because it makes the program larger
and run more slowly. Also, ON ERROR cannot avoid the long wait the BIOS
imposes. There are several solutions to this problem.
One is to print a flashing message at the bottom of the screen that says
something like, "Turn on the printer!" immediately before printing, and
then clear the message afterward:
LOCATE 25, 1
COLOR 23
PRINT "Turn on the printer!";
LPRINT Some$
COLOR 7
PRINT SPC(20)
If the printer is in fact on line and ready, the message will be displayed
and cleared so quickly that it is not likely to be noticed. Otherwise, the
operator will see the message and take the appropriate action.
This technique can be enhanced to instead test the printer, before
sending any data. The most reliable way I have found to test a printer is
to first send it a CHR$(32) space character, and if that is accepted print
a CHR$(8) backspace to cancel the original space. A further enhancement
alters the BIOS printer timeout values stored beginning at address &H478.
The combined demonstration and function that follows performs this service
using CALL Interrupt to circumvent BASIC's normal error handling routine.
DEFINT A-Z
DECLARE SUB INTERRUPT (IntNo, InRegs AS ANY, OutRegs AS ANY)
DECLARE FUNCTION LPTReady% (LPTNumber)
'$INCLUDE: 'REGTYPE.BI'
LPTNumber = 1
IF LPTReady%(LPTNumber) THEN
PRINT "The printer is on-line and ready to go."
ELSE
PRINT "Sorry, the printer is not available."
END IF
END
FUNCTION LPTReady% (LPTNumber) STATIC
DIM Regs AS RegType 'for CALL INTERRUPT
LPTReady% = 0 'assume not ready
Address = &H477 + LPTNumber 'LPT timeout address
DEF SEG = 0 'access segment zero
OldValue = PEEK(Address) 'save current setting
POKE Address, 1 '1 retry
Regs.AX = 32 'first print a space
Regs.DX = LPTNumber - 1 'convert to 0-based
CALL INTERRUPT(&H17, Regs, Regs) 'print the space
Result = (Regs.AX \ 256) OR 128 'get AH, ignore busy
Result = Result AND 191 'and acknowledge
IF Result = 144 THEN 'it worked!
Regs.AX = 8 'print a backspace
CALL INTERRUPT(&H17, Regs, Regs) ' to undo CHR$(32)
LPTReady% = -1 'return success
END IF
POKE Address, OldValue 'restore original
' timeout value
END FUNCTION
There are several important points worth mentioning here. First, you must
never use zero for the printer timeout value, or the timeout will be a *lot*
longer than you anticipated. A value of zero tells the BIOS to continue
trying indefinitely, and is equivalent to using the DOS MODE LPT1: command
with the ",p" argument.
Another point is that you should not use this function many times in a
row, without ever printing anything. All modern printers provide a buffer,
which accepts characters as fast as the computer can send them. If the
buffer fills with spaces and backspaces before any printable characters are
sent, it may be impossible to clear the buffer. Therefore, you should
perform the printer test only once or twice, just before you actually need
to begin printing.
EGA AND VGA DATA
================
The seven bytes starting at address &H484 hold information about an
installed EGA or VGA display adapter. This data should not be relied upon
until you have determined that the adapter is in fact an EGA or VGA. The
Monitor function shown in Chapter 6 can be used for this.
The first byte holds the number of rows currently displayed on the
screen. The next word at addresses &H485 and &H486 tells how high each
character is in scan lines. For a normal 80 by 25 line screen this value
will be 16. After using WIDTH , 43 or WIDTH , 50 the height of each
character is 8 scan lines. Notice that this value also includes the
spacing between each line. Curiously, two bytes are set aside to hold this
value, even though it is extremely unlikely that any video mode would ever
require a number larger than 255.
The only other information you are likely to find useful in this data
area is the amount of installed memory on the EGA or VGA adapter card.
Bits 5 and 6 at address &H487 hold the number of 64K banks, and the code
that follows shows how to turn this into a meaningful number:
DEF SEG = 0 'look in segment zero
Byte = PEEK(&H487) 'get the byte
Byte = Byte AND 96 'keep what we need (96 = 1100000b)
Byte = Byte \ 32 'shift the bits right five places
Byte = (Byte + 1) * 64 'add 1 because 0 means 64K
PRINT "This EGA/VGA adapter has"; Byte; "K memory"
After reading the EGA Features byte (listed earlier in Figure 10-1), the
statement Byte = Byte AND 96 masks off all of the bits that are irrelevant.
Byte is then divided by 32 to slide those bits into the lowest position.
The number that results is coded such that 0 means 64K of installed video
memory, 1 means 128K, 2 means 192K (which is never really possible), and
3 indicates 256K. Because this value is zero-based, 1 is added to Byte
before multiplying by 64.
MISCELLANEOUS DATA
==================
The 16-byte data area that begins at address &H4F0 is called the inter-
application communications area, and it is available for any arbitrary use
by a program. One possibility is for passing just a few parameters between
separate programs, instead of having to use COMMON and CHAIN. Although
this data area has been available since the original IBM PC was introduced,
there is a risk involved with using it because it is possible that another
program or TSR has stored information there. Chapter 9 described using the
last 96 bytes in the display adapter's memory, which is both a larger
buffer and is probably safer to use.
The byte at address &H500 is used as a flag by the BIOS Print Screen
service to detect when it is busy. When you press Shift-PrtSc, the BIOS
routine that handles that key sets this byte to a value of 1 before
beginning to print the screen. This way if you press Shift-PrtSc again
before it has finished printing, the second request can be ignored. When
the printing has completed the flag is then reset to zero.
You can set this flag manually to disable the action of the PrtSc key,
and then reenable it again later:
DEF SEG = 0
POKE &H500, 1
.
.
POKE &H500, 0
In fact, you must be *sure* to reenable PrtSc before ending your program if
you have disabled it. Otherwise, that key will be disabled until the PC
is rebooted.
The last low memory address I'll describe is also one of the most
potentially useful. For systems that have only one diskette drive, the
byte at address &H504 tells which drive (A or B) is currently active. In
this case, that drive serves as both A and B. Most PC users are familiar
with DOS' infamous "Insert disk for drive B" message. This message is
displayed whenever you attempt to access one of the logical drives while
the other is currently active.
The problem is that this message will ruin an otherwise attractive
screen design, and you have no control over where or if the message is
displayed. Fortunately, you can determine if only one drive is available,
and also which is currently active. Even better, you can set this byte to
reflect either drive, and thus avoid the intervention by DOS.
If the byte at address &H504 is currently zero, then drive A is active;
a value of 1 indicates drive B. The short complete program that follows
shows how to detect which drive is current.
DEF SEG = 0
Floppies% = (PEEK(&H410) AND 192) \ 64 + 1
PRINT "This PC has"; Floppies%; "floppy disk drive(s)."
IF Floppies% = 1 THEN
PRINT "The disk is now acting as drive ";
CurDrive% = PEEK(&H504)
IF CurDrive% THEN
PRINT "B"
ELSE
PRINT "A"
END IF
END IF
To change from drive A to B simply use POKE &H504, 1, assuming that the
current DEF SEG value is already zero. Likewise, to change from B to A you
will use POKE &H504, 0. Of course, you must also prompt the user to change
disks as DOS would. But at least you can control how the prompt message
is displayed. If you do switch drives behind DOS' back, it is up to you
to prompt the user to exchange disks as necessary, and also to ensure that
files are updated and closed correctly before each switch.
INPUT/OUTPUT PORTS
==================
Besides the low memory addresses that are reserved for BIOS and DOS uses,
every PC also has a collection of Input/Output (I/O) ports. Like memory,
ports are addressed by number, and data may be read from or to written to
them. In truth, some ports are write-only, others may only be read, and
still others can be read and written.
Where conventional memory is often used by the operating system to hold
flags, status words, and other values, ports are used to actually control
the hardware. For example, port number &H3F2 controls the diskette drive
motors, and appropriate OUT commands to that port can turn the motor for
any drive on or off.
For the most part, you should not experiment with the ports unless you
know what they are for, and which values are appropriate. As an example,
it is possible to damage your monitor by sending incorrect values through
the display adapter controller ports. Two useful ports I will describe
here control the PC's speaker and the keyboard.
Although BASIC offers the SOUND and PLAY statements, using them can
quickly increase the size of a program. Both of these commands can operate
in the background, thereby continuing to produce sound after they return
to your program. As you can imagine, this requires a lot of code to
implement. An informal test showed that adding a single SOUND statement
increased the program size by more than 11K. Therefore, if you do not need
the ability to have tones play in the background, the combination
demonstration and subprogram that follows can be used in place of SOUND.
Besides avoiding the code to plays tones as a background task, this routine
also avoids SOUND's inclusion of floating point math.
DEFINT A-Z
DECLARE SUB BSound (Frequency, Duration)
CLS
PRINT "Sweep sound"
FOR X = 1 TO 10
READ Frequency
CALL BSound(Frequency, 1)
NEXT
DATA 100, 200, 300, 400, 600, 900, 1200, 1500, 1800, 2100
PRINT "Press a key for more..."
WHILE INKEY$ = "": WEND
PRINT "Telephone"
FOR X = 1 TO 10
CALL BSound(600, 1)
CALL BSound(800, 1)
NEXT
PRINT "Press a key for more..."
WHILE INKEY$ = "": WEND
PRINT "Siren"
FOR X = 1 TO 2
FOR Y = 600 TO 1000 STEP 15
CALL BSound(Y, -1) 'negative values leave
NEXT ' the speaker turned on
FOR Y = 1000 TO 600 STEP -15
CALL BSound(Y, -1)
NEXT
NEXT
CALL BSound(600, 1) 'force the speaker off
SUB BSound (Frequency, Duration) STATIC
IF Frequency < 33 THEN EXIT SUB
IF NOT BeenHere THEN 'do this only once for a
BeenHere = -1 ' smoother sound effect
OUT &H43, 182 'initialize speaker port
END IF
Period = 1190000 \ Frequency 'convert to period
OUT &H42, Period AND &HFF 'send it as two bytes
OUT &H42, Period \ 256 ' in succession
Speaker = INP(&H61) 'read Timer port B
Speaker = Speaker OR 3 'set the speaker bits on
OUT &H61, Speaker
DEF SEG = 0
FOR X = 1 TO ABS(Duration) 'for each tick specified
ThisTime = PEEK(&H46C) ' count changes again
DO 'wait until the timer
LOOP WHILE ThisTime = PEEK(&H46C)
NEXT
IF Duration > 0 THEN 'turn off if requested
Speaker = INP(&H61) 'read Timer port B
Speaker = Speaker AND &HFC 'set the speaker bits off
OUT &H61, Speaker
END IF
END SUB
The BSound routine accepts the same frequency and duration arguments as
BASIC's SOUND statement. Each time it is called it calculates the
appropriate period based on the incoming frequency, which is what the timer
ports expect. (Period is the reciprocal of frequency. Here, the period
is related to the PC's clock frequency of 1,190,000 Hz.) BSound then turns
on the speaker, waits in a loop for the specified duration, and finally
turns off the speaker before returning.
Two extra steps are required to create a smooth effect when BSound is
called rapidly in succession. One is that the speaker port is initialized
only once, the very first time BSound is called. The other step lets you
optionally leave the speaker turned on when BSound returns, to avoid the
choppiness that otherwise results with sounds like the siren effect. To
tell BSound to leave the speaker on, use an equivalent negative value for
the Duration parameter. Just be sure to call BSound once again with a
positive duration value, or use the same set of INP and OUT statements
that BSound uses to turn the speaker off. This is shown in the last
demonstration that creates a siren sound.
KEYBOARD PORTS
There are several ports associated with the keyboard, and one is of
particular interest. The enhanced keyboards that come with AT-class and
later computers allow you to control how quickly keystrokes are repeated
automatically. There are actually two values--one sets the initial delay
before keys begin to repeat, and the other establishes the repeat rate.
By sending the correct values through the keyboard port, you can control
the keyboard's "typematic" response. The complete program that follows
shows how to do this, and Table 10-3 shows how the delay and repeat rate
values are determined.
OUT &H60, &HF3 'get the keyboard's attention
FOR D& = 1 TO 100: NEXT 'brief delay to give the hardware time to settle
Value = 7 '1/4 second initial delay, 16 CPS
OUT &H60, Value
AT-style keyboard delay and repeat rates
========================================
initial delay ---> 0.25 0.50 0.75 1.00
==== ==== ==== ====
30 characters per second: 0 20 40 60
16 characters per second: 7 27 47 67
8 characters per second: F 2F 4F 6F
4 characters per second: 17 37 57 77
2 characters per second: 1F 3F 5F 7F
NOTE: All values are shown in Hexadecimal.
Table 10-3: Sample values for setting the initial delay and repeat rate on
an AT-style keyboard.
Table 10-3 shows only some of the possible values that can be used.
However, you can interpolate additional values for delay times and repeat
rates between those shown.
SUMMARY
=======
This chapter explained what the BIOS low memory data area is, and also
discussed many of the addresses that are useful to application programs.
A number of practical examples were given, including useful PEEK and POKE
replacements that operate on data a word, rather than a byte, at a time.
A simple binary conversion function was shown, to help you determine the
correct values to use with AND and OR.
You learned how to exchange serial and parallel port addresses, and how
to access communications ports 3 and 4 which BASIC normally does not allow.
Exchanging printer ports lets you access any printer as LPT1, perhaps to
avoid having to rewrite a large program that relies on existing LPRINT
statements. Other useful printer data that can be accessed is the BIOS
timeout value, and a routine was shown for testing the printer status
without the usual delay.
The equipment list word was described in detail, showing how to
determine the number of diskette drives and other peripherals that are
installed. Another useful routine showed how to determine if drive A or
B is active on a one-floppy system, and also how to change the current
status of that drive. The various keyboard status bits were also
described, and code fragments showed how to read and set the current state.
Finally, you learned how the hardware ports are read and written using
INP and OUT commands. One example produced sound with much less generated
code than BASIC's SOUND, and another showed how to alter the typematic rate
on enhanced (AT) keyboards.
The next chapter explores using CALL Interrupt in great detail, using
many examples that show how to access DOS and BIOS system services.