A timely look at poke and peek. (BASIC programing) (column)
by Tom Campbell
Just a few years ago no serious microcomputer BASIC programmer would have been without a dogeared list of POKE and PEEK locations. Nowadays, POKE and PEEK commands are less often used, thanks to progress made by programming languages like QuickBASIC.
For those who aren't familiar with these commands, POKE writes a byte to memory, and PEEK returns the value of a byte in memory. POKE, as you might imagine, has the potential to crash a program with a single misstep. PEEK is harmless, but it's useful only if you know exactly what you're looking for. Both of these commands are complicated by the 8086's segmented addressing scheme.
As you probably know, the 8086 thinks in terms of 64K segments. You'll see the results of this everywhere. COM files, for example, must be 64K or less in size; until recently, QuickBASIC itself was limited to 64K of data, including strings; EMS memory can bank-switch only 64K of memory at a time; and so on. POKE and PEEK are no different. The statement
'Don't try this yet
POKE &H4F0,1
writes the value 1 to the memory location at offset 04F0h (don't try this POKE yet). Because POKE writes to a byte value, you can only poke a value of 0-255 into a single location. The PEEK function does the opposite: It lets you read the value of a byte in memory. This prints the value of the byte at offset 04F0h:
PRINT PEEK(&H4F0)
Early versions of BASIC had to fit into 4K, and a user was lucky to have 16K of RAM installed. Much of what we now take for granted in the BASIC language, like graphics and music, was unavailable. Users had to add to BASIC by writing machine-language routines with an assembler that would be poked into memory and executed directly with CALL.
This practice is now discouraged because it almost guarantees that the program won't work on other computers - or even your own, if the operating system changes.
Many pokes are obsolete under OS/2, with the exception of video memory and some system variable locations.
With these caveats aside, there are still good reasons to use POKE and PEEK. One such reason is demonstrated in this month's program, which times the execution of programs from the DOS command line.
The timer program (which appears at the end of this column) has two parameters: START and STOP. TIMER START pokes the current time into a small section of memory called, with typical IBM grandiosity, the Intra-Application Communications area, IAC, a scratch pad of 16 bytes for use in situations just like this one. I must warn you that all applications have equal access to this region of memory, so some programs will corrupt the IAC and make Timer fail by feeding it bad information.
Enter the command TIMER START; then run whatever program (or programs) you wish to time. When the program has run, issue a TIMER STOP command at the DOS command line to retrieve those bytes; they're both converted to time values and the difference (the elapsed time) is displayed onscreen. What's great about this method is that it requires zero bytes of your free RAM.
Note that the largest address you can POKE or PEEK is 0FFFFh, or 65,535 decimal. The 64K barrier strikes again. Limiting POKE to the first 64K of memory is unacceptable now, but back in 1980, when the PC's BASIC was being written, it made sense. The 64K POKE provided a certain measure of compatibility with existing BASIC programs. But earlier versions of Microsoft BASIC were for 8-bit machines with only 64K of address space, so how could the new BASIC address the PC's megabyte of RAM?
The solution was DEF SEG. The mysterious DEF SEG gives us a way. to read or write anywhere in memory. On the 8086, an address always consists of a segment and an offset. The segment is a 16-bit value that you multiply by 16 and add to the offset. DEF SEG lets you set the segment to write to with POKE or read from with PEEK. This is easier to explain through examples.
In the first example, we'll set the segment address to 0B00h, which is the start of video memory for monochrome monitors, or 0B800h, the start of color memory. As you may know video memory consists of a byte for each character on the screen plus a byte for its attribute (bold, blinking, colors, and so forth). The character bytes and attribute bytes alternate. The following will burn 80 happy faces directly into screen memory, using the current video attribute.
'(Change this to &HB800 for 'color systems.) DEF SEG = &HB000 'Starting at the base of video 'memory, FOR N = 0 TO 159 STEP 2 'Poke a happy face into 'each screen location, 'skipping every other 'byte, where the attribute 'appears. POKE N, 1 'Loop through the entire line. '0-159 represents 160 locations. '160/2 is 80, the number of 'columns across. NEXT
To see PEEK in action, let's examine the date of your computer's ROM BIOS. All true compatibles put this date where IBM put it, which is in ASCII form in the eight bytes starting at segment 0F000h, offset 0FFF5h.
Such addresses are customarily written as two 16-bit numbers wit a colon between them, as in F000:FFF5. Our program must convert these byte values to a string using CHR$, which takes an ASCII byte and returns its character representation. Here's the program.
PRINT "The ROM date"; PRINT "of this computer" PRINT "is:"; 'The address to search starts at 'hex F000:FFF5, so set the segment 'accordingly: DEF SEG = &HF000 'The offset is FFF5h; it's 'eight bytes' worth. FOR N = &HFFF5 TO &HFFFC 'PEEK returns a number 'in the range 0-255 in 'memory location at 'F000h, with an offset of N. 'CHR$ makes it a character. PRINT CHR$(PEEK(N)); NEXT
In the case of my ancient IBM AT, purchased in 1985, this program prints
The ROM date of this computer is:01/10/84
On my trusty 386 clone, my 24th birthday appears
The ROM date of this computer is: 03/11/86
Back to Timer. Compile the program to an EXE file, and find a program whose execution you want to time.
TIMER
' TIMER.BAS -- Command line utility to time program execution
' Use this segment for POKEs and PEEKs. DEF SEG = 0 ' Offset address of the Intra-Application Communications area--IBM's ' 16-byte scratchpad for programs like this. CONST IAC% = &H4F0
' Force the command line to uppercase. SELECT CAUSE UCASE$ (COMMAND$)
' "START" means start the timer and poke the time in to the IPC.
CASE "START"
'Strip the colons out of the time, which is in the format
' "HH:MM:SS". Leading 0s guarantee this format.
StartTime$ = LEFT$(TIME$, 2) + MID$ (TIME$, 4 , 2) + RIGHT$(TIME$, 2)
' Loop through the string, poking the ASCII values into the start
' of the IPC.
FOR Index = 0 to 5
' ASC converts each letter in the string to a 1-byte value.
' Since strings start at index 1, not 0, add 1 to Index.
POKE IAC% + Index, ASC(MID$(StarTime$, Index + 1, 1))
NEXT Index
' "STOP" reads the starting value from the IPC back into a string
'variable, then splits out the hours, minutes, and seconds.
CASE "STOP"
' Capture the ending time of the program execution.
StopTime$ = TIME$
' Initialize the string to be built up from the poked ASCII values.
StartTime$ = " "
FOR Index = 0 TO 5
PEEK gets each character of the string as a binary (ASCII) value.
' CHR$ converts it to a character so it can be concatenated to
' the string.
StartTime$ = StartTime$ + CHR$ (PEEK(IAC% + Index))
NEXT Index
' Convert the starting and ending times to seconds, subtract,
' then convert to Hours, Minutes, and Seconds
BeginTime - VAL(LEFT$(StartTime$, 2)) * 3600 + VAL(MID$(StartTime$, 3,2)) * 60 + VAL (RIGHTS(StartTime$, 2))
EndTime = VAL(LEFT$(StopTime$, 2)) * 3600 + VAL(MID$(StoptTime$ f,2)) * 60 + VAL(RIGHTS (StopTime$, 2))
Seconds - EndTime - BeginTime
Hours - INT(Seconds / 3600): Seconds = Seconds - Hours 80-00 &*
Minutes = INT (Second /60): Seconds = Seconds = Minutes *60
'Only print hours it not 0.
IF Hours > 0 THEN
PRINT Hours;
' Make sure there's subject/verb agreement; avoid cases like
" 1 hours".
IF Hours = 1 THEN PRINT "hour"; ELSE PRINT "hours'"
END IF
' A little tricker: print a comma after the hours only if hours were
' printed. Then print minutes, if not 0.
IF Minutes > 0 THEN
IF Hours > 0 THEN PRINT ",";
PRINT Minutes;
IF Minutes = 1 THEN PRINT "minute"; ELSE PRINT "minutes";
END IF
' Again, use a leading comma only if a) there's not already a
' comma, and b) one is needed. If both hours and minutes are
' 0, no comma is needed.
IF Seconds > 0 THEN
IF NOT ((Hours = 0) AND (Minutes = 0)) THEN PRINT ",";
PRINT Seconds;
IF Seconds = 1 THEN PRINT "second" ELSE PRINT "seconds"
END IF
CASE ELSE
PRINT "TIMER by COMPUTE times a program's execution from the command line."
PRINT "Use TIMER START before running the program, and TIMER STOP after."
PRINT "Example:"
PRINT " REM First, initialize the timer:"
PRINT " C:\>TIMER START"
PRINT " Next, run a program:"
PRINT " C:\>SORT < DATABASE.PRN > NEWBASE.PRN"
PRINT " Finally, display the execution time:"
PRINT " C:\>TIMER STOP"
END SELECT