Part 4 of 11 -- Simple Assembly for Atari BASIC - Implement DPEEK()
6502 Assembly Atari 8-bit Atari BASIC Mac/65
Part 1 - Introduction
Part 2 - Learn 82.7% of Assembly Language in About Three Pages
Part 3 - The World Inside a USR() Routine
Part 4 - Implement DPEEK()
Part 5 - Implement DPOKE
Part 6 - Various Bit Manipulations
Part 7 - Convert Integer to Hex String
Part 8 - Convert Integer to Bit String
Part 9 - Memory Copy
Part 10 - Binary File I/O Part 1 (XIO is Broken)
Part 11 - Binary File I/O Part 2 (XIO is Broken)
Since one byte can hold the value 0 to 255 a value larger than 255 requires two bytes. The second byte takes the place value 256. Just as Base10 has “ones” values 0 to 9, and then “tens” value for the next position, so the two bytes provide a Base256 value – “ones” value 0 to 255 in the first position and then 256 as the “tens” in the second position.
The two-digit value limit for Base10: multiply the maximum base value by 10 for the “tens” value and add the maximum base value of the “ones”, or 9 * 10 + 9 = 99.
The same applies for Base256: multiply the maximum base value by 256 for the “tens” and then add the maximum base value as the “ones”, or 255 * 256 + 255 == 65,535. (Or, in hex this is $FF * $100 + $FF == $FFFF.) So, the real value limit of 16-bits starts at 0 and ends at 65,535 or $FFFF.
The 64K address space of an 8-bit computer is described by a 16-bit value. So, the Atari environment is liberally sprinkled with 16-bit values as addresses, pointers, and larger integer values. Manipulating 16-bit values is complementary to working in the Atari computing environment, but Atari BASIC does not provide a direct and easy method for this.
OSS BASIC XL provides the Dpeek() function to perform a 16-bit, two-byte PEEK of the value at a specified memory location. This can also be duplicated in regular Atari BASIC, although slower. In BASIC XL the action:
Value = Dpeek( Address )
is frequently seen in Atari BASIC programs expressed as:
VALUE = PEEK( ADDRESS ) + 256 * PEEK( ADDRESS + 1 )
That’s not very complicated. A little programming grease mixed with Atari BASIC’s ability to GOSUB to a variable produces this reusable subroutine:
10 DPEEK=28000 20 ADDRESS=560:GOSUB DPEEK 30 ? "DPEEK(560)= ";VALUE 40 END . . . 27997 REM IMPLEMENT DPEEK 27998 REM INPUT = ADDRESS 27999 REM OUTPUT = VALUE 28000 VALUE=PEEK(ADDRESS)+256*PEEK(ADDRESS+1) 28001 RETURN
The routine is simple; just one real line of BASIC code. But, execution in Atari BASIC is fairly slow, since it includes a floating point multiplication. Infrequent slothfulness is forgivable, but repeated use causes obvious performance drag. What this problem needs is a little assembly language propulsion...
DPEEK Mac/65 Assembler Code
0100 ; DPEEK 0105 ; 0110 ; Return the 16-bit contents 0115 ; at the specified address. 0120 ; 0125 ; USR 1 arguments: 0130 ; Addr == address of value. 0135 ; 0140 ; USR return value is 16-bit 0145 ; contents of address. 0150 ; 0155 ; Use the FR0 FP register. 0160 ; The return value for BASIC 0165 ; goes in FR0. 0170 ; No FP is used so all of FR0 0175 ; (and more FP registers) can 0180 ; be considered available. 0185 ; 0190 ZRET = $D4 ; FR0 $D4/$D5 Return value 0195 ZARGS = $D5 ; $D6-1 for arg Pulldown loop 0200 ZADDR = $D6 ; FR0 $D6/$D7 Address 0205 ; 0210 .OPT OBJ 0215 ; 0220 ; Arbitrary. This is relocatable. 0225 ; 0230 *= $9700 0235 ; 0240 INIT 0245 PLA ; argument count 0250 TAY 0255 BEQ EXIT_ERR ; shortcut for no args 0260 ASL A ; now number of bytes 0265 TAY 0270 CMP #$02 ; Address 0275 BEQ PULLDOWN 0280 ; 0285 ; Bad args. Clean up for exit. 0290 ; 0295 DISPOSE ; any number of args 0300 PLA 0305 DEY 0310 BNE DISPOSE 0315 ; 0320 EXIT_ERR ; return "error" 0325 STY ZRET ; Y is Zero 0330 STY ZRET+1 ; 0335 RTS ; bye. 0340 ; 0345 ; This code works the same 0350 ; for 1, 4, 8 ... arguments. 0355 ; 0360 PULLDOWN 0365 PLA 0370 STA ZARGS,Y 0375 DEY 0380 BNE PULLDOWN 0385 ; 0390 ; Y is already zero here. 0395 ; 0400 LDA (ZADDR),Y 0405 STA ZRET 0410 INY 0415 LDA (ZADDR),Y 0420 STA ZRET+1 0425 ; 0430 RTS ; bye. 0435 ; 0440 .END
In this first example I’ll walk through all the setup and safety checks. This will be similar for most of the other utilities. Some programmers would consider that there is much more code here than required. The safety checks look like overkill, because the working code for this particular utility is so short. Protecting the BASIC programmer from torpedoing the system is important enough to justify protection. From the point of view of execution timing this overhead is inconsequential compared to the speed of BASIC.
First of all, the routine is relocatable which means the code makes no absolute references to locations within itself. All branches are relative to the current location, so the code could execute from almost anywhere in memory. However, it does need a couple of fixed locations for working values in the program:
0155 ; Use the FR0 FP register. 0160 ; The return value for BASIC 0165 ; goes in FR0. 0170 ; No FP is used so all of FR0 0175 ; (and more FP registers) can 0180 ; be considered available. 0185 ; 0190 ZRET = $D4 ; FR0 $D4/$D5 Return value 0195 ZARGS = $D5 ; $D6-1 for arg Pulldown loop 0200 ZADDR = $D6 ; FR0 $D6/$D7 Address
The program needs two values – the address from which to PEEK the 16-bit value, and a place to put the 16-bit value for BASIC to reference. Specifically, the code is using the Page Zero locations. “Page Zero” means all the address locations where the high byte of the address is $00. (The 256 locations from address $0000 to $00FF). Page Zero locations are chosen for several reasons:
- 6502 instructions referencing Page Zero are one byte shorter and usually execute faster than instructions referencing other pages.
- The 6502 has useful addressing modes that only work for Page Zero references.
- BASIC already defines a place in Page Zero for the value the machine language routine returns to BASIC.
Since Page Zero locations are so useful they are also highly contested. The Atari OS defines and uses just about every byte in the first half of Page Zero. BASIC and the Floating Point routines use almost all of the second half of Page Zero. As it turns out the locations used by this machine language routine are “claimed” by the Floating Point routines. However, as long as the machine language routine does not need to use the Floating Point routines then these locations are free for use.
Now to the working code. The routine begins by pulling the argument count from the stack and checking for zero arguments. If it finds no arguments to process then the routine branches to another location for an early exit. Yes, the TAY instruction is not needed to correctly branch for no arguments. The PLA instruction sets the zero flag when the argument count popped from the stack is zero.
However, the exit code will use the Y register to return an error value (which is zero) to BASIC:
0240 INIT 0245 PLA ; argument count 0250 TAY 0255 BEQ EXIT_ERR ; shortcut for no args
Next, the routine converts the number of arguments into the number of bytes to pull from the stack. It uses ASL which is the same as multiplying the value in the Accumulator times two. This is stored in the Y register which is used as an index for later loops. The routine verifies the argument count is correct (only 1 argument – the address to PEEK) which is 2 bytes. If this is correct the routine branches to the code that will pull the stack values and place them in Zero page memory for use later:
0260 ASL A ; now number of bytes 0265 TAY 0270 CMP #$02 ; Address 0275 BEQ PULLDOWN
If the argument count is incorrect then the routine discards the argument values on the stack, so it can safely exit. Remember the Y register already contains the number of argument bytes on the stack:
0295 DISPOSE ; any number of args 0300 PLA 0305 DEY 0310 BNE DISPOSE
Next, the routine falls into the exit section. Earlier, if there were no arguments the routine branched here directly. Note that if there are no arguments the Y register contained 0 when it branched here directly, and at the conclusion of cleaning up the stack in the DISPOSE loop the Y register will also be 0. So, in either case of bad arguments, too many or too few, the return value to BASIC is cleared to 0:
0320 EXIT_ERR ; return "error" 0325 STY ZRET ; Y is Zero 0330 STY ZRET+1 ; 0335 RTS ; bye.
Clearing the return value really isn’t necessary and isn’t exceedingly useful beyond insuring random values are not returned to BASIC in the case of an error. Returning a real error would require the USR() pass another argument from BASIC that the routine would use to indicate success or failure. However, the DPEEK action is so simple that this level of error detection begins to enter the arena of silly. The error detection shown here already leans toward overkill and is done for design consistency with the other utilities covered later.
At this point the arguments are correct. The routine pulls the values from the stack and places them in Page Zero locations $D6/$D7 referred to as ZADDR. On entry to this point in the code the Y register contains the number of bytes to pull from the stack (always 2 for this routine). The loop pulls them off the stack and places them into memory descending as it goes, because the high byte is pulled first, then the low byte.
The base address is not ZADDR, but ZARGS, a value defined one byte less than ZADDR. This is because the Y value will be used as an index from the number of argument bytes (2, 4, 6, etc.) counting down to 1, not to 0. Counting backwards results in the stack values placed in memory in the correct low byte, high byte order used by the 6502. When Y reaches 0 it falls out of the loop:
0360 PULLDOWN 0365 PLA 0370 STA ZARGS,Y 0375 DEY 0380 BNE PULLDOWN
This code works for any number of arguments as long as the destination can be sequential bytes in memory. However, this is admittedly overkill for only one argument. More explicit code that directly pulls one 16-bit argument from the stack requires the same number of instructions, but is one byte less (and executes faster). Just for reference:
0360 PULLDOWN 0365 PLA 0370 STA ZADDR+1 ; high byte first. 0375 PLA 0380 STA ZADDR ; low byte next.
The actual work to perform the double byte peek is just the five instructions before the final RTS. The routine reads two bytes through the address contained in ZADDR ($D6/$D7) and copies the bytes to the return value, ZRET ($D4/$D5):
0400 LDA (ZADDR),Y 0405 STA ZRET 0410 INY 0415 LDA (ZADDR),Y 0420 STA ZRET+1
After the routine exits Atari BASIC converts the value stored in locations $D4/$D5 (ZRET in the code) into a floating point value. This becomes the return value from USR(). So, in the following BASIC statement the variable X is assigned the value taken from $D4/$D5:
250 X=USR(DPEEK,560): REM DISPLAY LIST ADDRESS
Below are source files and examples of how to load the machine language routine into BASIC included in the disk image and archive:
DPEEK File List:
DPEEK.M65 Saved Mac/65 source
DPEEK.L65 Mac/65 source listing
DPEEK.T65 Mac/65 source listed to H6: (linux)
DPEEK.ASM Mac/65 assembly listing
DPEEK.TSM Mac/65 assembly listing to H6: (linux)
DPEEK.OBJ Mac/65 assembled machine language program (with load segments)
DPEEK.BIN Assembled machine language program without load segments
MKDPEEK.BAS BASIC program to create the DPEEK.BIN file. This also contains the DPEEK routine in DATA statements.
MKDPEEK.LIS LISTed version of MKDPEEK.BAS
MKDPEEK.TLS LISTed version of MKDPEEK.BAS to H6: (linux)
DPEEK.BAS BASIC program that loads DATA statements into a string.
DPEEK.LIS LISTed version of DPEEK.BAS
DPEEK.TLS LISTed version of DPEEK.BAS to H6: (linux)
The next task should be a test program to demonstrate using DPEEK in BASIC. But, since DPEEK isn’t much use without DPOKE, we will visit DPOKE first before using the machine language routines together in a BASIC program.
ZIP archive of files:
Tar archive of files (remove the .zip after download)
Therefore I tell you, do not be anxious about your life, what you will eat or what you will drink, nor about your body, what you will put on. Is not life more than food, and the body more than clothing? Look at the birds of the air: they neither sow nor reap nor gather into barns, and yet your heavenly Father feeds them. Are you not of more value than they? And which of you by being anxious can add a single hour to his span of life? And why are you anxious about clothing? Consider the lilies of the field, how they grow: they neither toil nor spin, yet I tell you, even Solomon in all his glory was not arrayed like one of these.