Open1, Findmn, Getsa,
I/O Management and
I/O (Input/Output), a computer's method of communicating with its peripherals, is one of the most machine-specific and potentially complex aspects of machine language programming.
Sending or receiving bytes to or from disk or tape drives and sending bytes to a printer are the most common I/O activities. A large part of a computer's ROM memory is usually devoted to managing I/O.
I/O is machine-specific because each manufacturer invents his own way of managing data, his own variations on the ASCII code, and his own disk or tape operating systems.
And I/O is complex because printers and disk and tape drives differ greatly in such things as how fast they can store bytes, how many bytes they can accept, and esoteric matters like timing, error checking, and special control signals.
ML programmers are frequently advised to perform 1/O operations in BASIC and then SYS, CALL, or USR into the ML after the hard part has been accomplished by the computer's operating system. This works well enough with small ML projects. But it can become awkward in a large ML program. LADS itself must open and close disk files pretty often. It would be inefficient to require LADS to fly down into an attached BASIC program for this. Also, large ML programs are easiest to save, load, and use if they are written entirely in ML.
Fortunately, we can access BASIC's ROM routines from within an ML program. Certain registers and pointers in zero page need to be set up, then we can JSR to open a file to a peripheral. After that, we can send or receive bytes from that file.
Since these routines are so machine-specific, we'll look at the Commodore techniques in this chapter. See Appendix C for an explanation of the Atari and Apple I/O techniques.
Some peripherals are intelligent and some are dumb. Commodore disk drives are highly intelligent-they've got large amounts of RAM and ROM memory. One consequence of this is that relatively little I/O computing needs to be done within the computer proper. A Commodore disk drive is a little computer itself. You can just send it a command, and it takes over from there.
The tape drives, though, are dumb. ROM intelligence within the computer must manage I/O to tape. Some printers aren't so dumb, but since you can choose from so many different models and brands, the computer just sends out a sequence of raw bytes when you print to a printer. Your BASIC or operating system makes no effort to control fonts, formatting, or any other special printer functions. You are expected to send any necessary printer control codes via your software. If the printer is equipped to TAB or justify text, that's up to the printer's ROM.
In the subprogram Openl, there are four Commodore-specific subroutines. In many respects, they are identical subroutines. Each opens a file to an external device in much the same way. Only the specifics differ. The first subroutine, OPEN1, starts communication with a disk file which will be read. That is, the source code will come streaming in from this file so that LADS can assemble it. This file will be referred to as file 1.
The second subroutine, OPEN2, opens file 2 as a write file. If the user includes the D NAME pseudo-op within his source code, the results of a LADS assembly, the object code, will be stored on disk in a file called NAME. OPEN2 makes the disk create this file.
The third subroutine, OPEN4, creates a simple write file to the printer. It, too, is similar to the others except that there is, of course, no filename.
Looking at OPEN1, the first event is a call to the CLRCHN subroutine within BASIC. All I/O (including that to the screen and from the keyboard) is governed by this opened-files concept in Commodore computers. The normal I/O condition is output to the screen and input from the keyboard. CLRCHN sets the computer to this condition. It is a necessary preliminary before any other opening or closing of files.
Resetting the Disk Program Counter
Next we close file # 1 (50-60). This resets the disk intelligence. As we shift from pass 1 to pass 2, we've been reading through file # 1 to bring in our source code. On pass 2, we want to start all over again with the first byte in the disk source file. It is necessary to close, then reopen, file #1 to force the disk intelligence to again point to that first byte in the file.
Next we must prepare some zero page file-manipulation pointers. We store the file number to FNUM, the device number (8 is the disk device number in Commodore computers) to FDEV, and the secondary address to FSECOND. All of this is precisely what we do in opening a file from BASIC with OPEN 1,8,3.
Then we have to point to the location of the filename within RAM. LADS holds filenames in a buffer called FILEN, so we put the low and high bytes of FILEN's address into the FNAMEPTR. Then, at last, we go to OPEN, the BASIC subroutine which opens a disk file.
The four zero page locations and the OPEN routine in ROM are all machine-specific. They are defined in the Defs subprogram. OPEN2 is identical except for a different filename, a different file number, and a different secondary address (which makes it a write file).
OPEN4, too, is identical except that the secondary address is ignored, the device number is 4 (for printers in Commodore computers), and there is no filename.
Line 430 reveals a fifth zero page location which must be POKEd before calling the OPEN subroutine in BASIC ROM. It holds the length of a filename. (Opening to a printer uses no filename, so a zero is put into FNAMELEN .)
Both of the other subroutines, OPEN1 and OPEN2, do not need to POKE FNAMELEN. It is POKEd just before LADS JSRs to either of them.
LOAD1, the final 1/O subroutine in this subprogram, is used with the assemble-from-RAM-memory version of LADS. In this case, the source code files are LOADed into RAM before they are assembled. This means that we need to imitate a typical BASIC LOAD of program files.
The LOAD subroutine within BASIC requires that the LOAD/VERIFY flag be set to LOAD (rather than VERIFY), that 8 be declared the device (disk), and that the name of the program to be loaded be pointed to. Then the machine-specific LOAD routine within BASIC is called. After that, the program (the source code) is loaded into the normal RAM address for BASIC programs.
Findmn: Table Lookup
This subprogram is similar to the Array subprogram: Both look through an array and find a match to a "source" word. Yet Findmn is simpler than Array. It doesn't need to check for word lengths. Also, the numbers (the values) associated with the words in the array are more simply retrieved. Findmn tries to find a mnemonic like LDA or BCC in a table of all 56 of the 6502 machine language mnemonics.
This table (or array) of mnemonic names is in the subprogram Tables at the very end of LADS source code. The mnemonics table starts off like this:
50 MNEMONICS .BYTE "LDALDYJSRRTSBCSBEQBCCCMP
60 .BYTE "BNELDXJMPSTASTYSTXINYDEY
and continues, listing all of the mnemonics.
This array of mnemonics is simpler and faster to access than our array of labels because it's what's called a lookup table. It has four characteristics which make it both easy to access and very efficient: It's a fixed field array (all items are three bytes long), it's static, it's parallel, and it's turbo-charged.
Charles Brannon, my colleague at COMPUTE! Publications, is a proponent of what he calls "turbo-charged code." He writes an ML program, gets the logic right, and then takes a cold look at things, especially at heavily used loops. Is the first CMP the one most often true in a series of CMPs? Or would it be faster to rearrange these CMPs in order of their probability of use? Should an Indirect Y addressing mode be replaced by an even faster structure such as self-modifying Absolute addressing? Would a lookup table be a possible replacement for some computed value? Sometimes, small changes can result in extraordinary gains in speed. For example, after LADS was finished and thoroughly tested, it took 5 minutes, 40 seconds to assemble itself (5K of object code).
A cold look, about five hours of work, and the resulting few minor changes in the source code brought that time down to its present speed for self-assembly: 3 minutes, 21 seconds. (This speed test was conducted with only the D name pseudo-op activated, on a Commodore PET/CBM 8032, with a 4040 disk drive, and involving far fewer comments than found with the source code as published in this book. The use of additional pseudo-ops, additional comments, or other computer/disk brands and models will result in different assembly speeds. The Apple has a faster disk drive, for example, and the LADS Apple version is even faster than the Commodore version.)
How does this mnemonics lookup table differ from the label array? They're both arrays, but the label array is a dynamic array. It changes each time you reassemble different source code. A lookup table, by contrast, is static: It never changes. It's a place where information is permanent and lends itself, therefore, to a bit of fiddling, a bit of turbo-charging.
A Special Order
First of all, in what order did we put these mnemonics? They're not in alphabetical order. In that case, ADC would be first. They're not in the numeric order of their opcodes either. Using that scheme, BRK would be first, having an opcode of 0. Instead, they're in order of their frequency of use in ML programming. The order wasn't derived from a scientific study-I just looked at them and decided that I used LDA more often than anything else. So I put it first.
The reason for putting them in order of popularity is that every line of source code contains a mnemonic. Every time a mnemonic is detected, it must be looked up. Since this lookup starts with the first three-letter word in the table (all mnemonics are three letters long) and works its way up the table, it makes sense to have the most common ones lowest in the table. They'll be found sooner, and LADS can continue with other things. It turns out that rearranging the order of the mnemonics in the table resulted in an increase in speed of considerably less than 1 percent, but everything helps. The principle is valid, even if it doesn't accomplish much in this case.
The second quality of a lookup table-parallelism-is rather significant to the speed of LADS. Right below the MNEMONICS table in the Tables subprogram are two parallel tables: TYPES and OPS. (See the Tables subprogram at the end of Chapter 9.) TYPES can be numbers from 0 to 9. It is handy to group mnemonics into these ten categories according to the addressing modes they are capable of using. Some mnemonics, like RTS, INY, and DEY, have only one possible addressing mode (they take no argument and have Implied addressing). They are all labeled type 0. The branching instructions, BNE, BEQ, etc., are obviously related in their behavior as well: They are type 8. This categorization helps the Eval subprogram calculate addressing modes. This table of TYPES parallels the table of MNEMONICS. That is, the first mnemonic (LDA) is type 1, so the number 1 is the first number in the table of TYPES. The fifth mnemonic in the MNEMONICS tables, BCS, is paralleled by the fifth number in the TYPES table, 8.
The Efficiency of Parallel Tables
What's the value of putting them in parallel? It allows us to use the Y or X Register as an index to quickly pull out the values in any table which is parallel to the primary lookup table, MNEMONICS. Once we've found a match within MNEMONICS, we can simply LDA TYPES,X to get that mnemonic's type. And we can also LDA OPS,X to get the opcode for that mnemonic. All this works because we INX after each failure to match as we work our way up through the MNEMONICS table. X will point to the right item in each of the parallel tables, after we find a match.
But now on to the actual lookup techniques which are used in the Findmn subprogram. As usual, we set our index counters, X and Y, before entering a loop. X gets $FF (40), so it will zero at the first INX at the start of the loop. Y gets 0. You can tell that this was the first subprogram written in LADS. Nowhere else can we achieve the elegant simplicity of calling a loop LOOP and the end of the routine END (390). After using them once, we'll have to come up with other names for loops and exits.
Anyway, we enter LOOP and look at the first character in the MNEMONICS table (60). If it matches the first character in the buffer LABEL (holding something like: LDA 15), we jump down to look for a match to the second, and then the final, character in the mnemonic. Otherwise, if there is no match, we INY INY INY to move up three characters in the MNEMONICS table and prepare to compare the first letter of the second mnemonic against our source mnemonic.
When looking something up, it saves time if you just test first characters before going on to whole-word tests.
Assuming a first characters match, MORE (150) compares the second characters. If they match, we go on to MOREL This time a failure to match results in two INYs because there was one INY at the start of MORE. MORE1 tests the third characters. If it fails, we only need one INY. In each case, a failure returns to LOOP. LOOP itself fails when it has exhausted all 56 mnemonics in the table and no match has been found. Since each attempt causes X in INX, we can test for the end of the table of 56 mnemonics by CPX #57 (120).
If we have exhausted the table, we jump back into the Eval subprogram where label definitions are evaluated. Since we didn't find a mnemonic as the first thing on a source code line, it must be a label like:
100 LABEL LDA 15
100 LABEL = 75
JMP for JMP
Note that we don't need to PLA PLA the return address of an RTS off the stack before JMPing back to Eval from this subprogram. That's because we JMPed here from Eval. Both possible returns to Eval will be JMPs. That makes it possible for us to JMP directly to Findmn from Eval. For speed, we can JMP back to two different places within Eval, depending on whether we did or did not find a mnemonics match.
Finding a match, however, sends us to the FOUND subroutine (300) where we check to see if there is a blank character or a zero (end of line) following the supposed mnemonic. If there isn't, that means we've got a label which looks like a mnemonic: INYROUTINE or BPLOT or something. We cant let that fool us. If there's a character in the fourth position, such words reveal themselves to be labels. If so, we go back to Eval via NOMATCH.
But let's say that all was well. It's not an address label, it's not an equate label, it's not a label disguised as a mnemonic. We've located a true mnemonic. All we have to do is pick its TYPE and OPCODE out of their tables and store them in their holding places, the variables TP and OP, and JMP back to EVAR in Eval. EVAR is a subroutine in Eval which examines the argument of a mnemonic to determine its addressing mode.
Getsa: The Simplest Routine
This subprogram has only one mission: to point to the starting address in the source code program. Here's what it points to:
10 * = 864
Getsa pulls off the first six bytes (in a Commodore disk program file) so that it can check to see if the seventh byte is the * character (120). If so, Getsa returns to the calling routine in Eval (200). If not, it prints the NO START ADDRESS error message and goes to FIN (190), the shutdown (return to BASIC) routine.
There are two fundamentally different versions of LADS. The version presented as object code (to be typed in) in this book assembles from disk-based source code. You create BASIC-like "programs" on disk, and then LADS reads them and assembles them without bringing any source code into RAM memory.
An easy modification to LADS, however, will allow it to assemble directly from source code within RAM memory. A few trivial changes to LADS' own source code and you can assemble a new, memory-based LADS. These changes are described between lines 430 and 640 of the Getsa source code printed at the end of this chapter. The changes are described in greater detail in Chapter 11, "Modifying LADS."
But this Getsa source code illustrates one way that your source code program can conditionally assemble. Notice line 210. The MEMSA and CHARIN routines below it will never be assembled. When LADS sees the FILE pseudo-op, it will immediately turn its attention to the Valdec source code. FILE shuts down the current file and switches to the named source file, ignoring any additional source code in the current file.
Thus, to assemble the "conditional" part of this source code, all you have to do is move FILE below the new source code. See the instruction in line 580 of this Getsa subprogram. That's how you do it to create a memory-based version of LADS.
Another way to conditionally assemble is to insert the NO pseudo-op, thus turning off object-code-to-memory-storage until the .O pseudo-op turns it back on. You could write your own .ND (no storage to disk) pseudo-op if you want to control assembly which is sending its object program to a disk drive. Another pseudo-op you could write would be something like .NA for No Assembly which would cause LADS to simply search down through source code (taking no actions other than building the label array) until it located a .A pseudo-op, turning all assembly back on. These ND, NA, and .A pseudo-ops aren't built into LADS, but would be easy to add if you felt you'd have a use for them.
Valdec: Number Conversion
Numbers such as the 15 in LDA 15 are held in ASCII code format within source programs. In other words, when LADS pulls in the 15, it doesn't get the number 15. It gets 1-5 instead. It gets the ASCII for 1 and the ASCII for 5: 49 and 53 decimal. (As an aside, 1 and 5 are $31 and $35 in hex. It's pretty easy to mentally convert ASCII hex to numeric form. Just drop the leading 3 from any hex ASCII number.)
What Valdec must do is turn 49 53 into the two-byte number OF 00 which the computer can recognize and work with. This is just a bit more complicated than it might seem. The complexity comes from the fact that the 1 in 15 is really 10 times 1. The Valdec subprogram which handles this ASCII-to-integer translation will have to multiply by 10,000 or 1000 or 10 or 1depending on the position of the ASCII digit. We don't need to worry about numbers higher than 65535 since ML doesn't often need to calculate higher than that. All addresses that the 6502 chip can reach are within that range, and two bytes cannot hold a larger number anyway. Therefore, multiplication by 10,000 will take care of any case we might come across.
And since 10,000 is just 10 X 10 X 10 X 10, we'll really only need a way of multiplying by 10 a maximum of four times. So all that's really needed is a multiply-by-10 routine that we can loop through as often as necessary. Lines 400-550 perform this operation.
But let's start at the start. Anything in LADS which calls upon Valdec for its services will have already set up the TEMP pointer to point to the first ASCII character in the number to be translated. Also, the number will end with a 0 delimiter. (This isn't the ASCII 0, which is $30. It's a true zero.)
After Valdec finishes, it leaves the results in the two-byte register called RESULT.
First Valdec finds the length of the ASCII number (50-90). Our example number, 15, would be two bytes long. Its length is stored in the variable VREND, and we then clean out the RESULT register by storing 0 into it (130-150). Then X (not the register, the variable) is stuffed with a 1 (170) so it can tell us how many times to loop through the times-ten routine for each digit. As we move from right to left, reading first the 5 then the 1 in 15, X will be raised. Coming upon the 5, X will be 1, and we'll perform no multiplication. The first thing the loop for multiplication does is DEX, so 1 becomes 0 and we exit the loop (250).
Coming upon the 1, X will tell us to go through the timesten routine once. In other words, we multiply 1 times 10 for a result of 10. This, added to 5, gives the 15 we're after.
But let's back up to where we were, at VALLOOP (180). We can take advantage of the fact that the ASCII code was designed so that the lower four bits in each ASCII numeral byte hold the actual number: $35 stands for 5. How do we extract the number $05 from $35? We could subtract $30. Even simpler is AND #$0F. AND turns bits off. Wherever a bit is off in the mask (the #$0F in this example), the bit will be off in the result:
||(ASCII for 5)
||(the four high bits are all off,
the four low bits are on-they
have no effect)
||(the answer we're after)
||($35, prepared to be stripped of its high bits by)
|($0F, the mask, turning bits off where the 0's
||($05, leaving the number we want)
Here we load in the rightmost character, the 5 in 15, the $35 in $31 $35. And strip off the 3, leaving the 5. Then that's stored in two temporary variables: RADD and TSTORE. Next we fill both of the high bytes of these variables with 0 (220-240). That makes them officially correct. Nothing lingers in their high bytes to confuse things later when we perform two-byte addition.
Now that our digit 5 is safely tucked away, we need to multiply it by 10 as many time as necessary. DEX lowers X. With this first character, X becomes 0, and we BEQ to the exit (330). When we come through this loop next time, holding the 1 in 15, X will become 1 and we'll therefore JSR TEN (270) one time, making 1 into 10.
Keeping Track of Position
After the subroutine TEN has multiplied the number in RADD (named for Result of ADDition) by 10, we transfer the result from RADD over to TSTORE (280-310). Why the transfer? Because in the 100's position, a digit would need to be multiplied by 10, twice. The 2 in 215 would have to be 2 times 10 times 10. So TSTORE has to keep a running total of the results achieved by the TEN subroutine. TEN uses RADD during multiplication. Obviously, a second two-byte variable will have to keep track of the total as, more than once, we multiply the larger digits by 10.
Another running total, the result of all Valdec's efforts, is kept in the variable RESULT. That will ultimately hold our final answer. But each time we achieve an interim answer on a single digit, we JSR VALADD (350) to add the results of that digit's multiplication to RESULT (570-640).
Meanwhile, back up at line 360, we DEY to point to the next higher digit, the digit next to the left. And DEC VREND to see if we've reached the end of our ASCII number and cannot RTS. If not, we go back up and load in the next digit, continuing to add to the running total in RESULT.
The multiply-by-ten routine called TEN (410) is worth a brief examination. Let's imagine that we have put a 1 into RADD (200) and we're going through the TEN loop once, multiplying it by 10. We clear the carry. ASL shifts each bit in RADD (the low byte of this two-byte number) to the left by 1. The interesting thing is that the seventh bit goes into the carry. Then we ROL RADD+1, the high byte, which rotates each bit to the left. This is the same as the ASL shift to the left. The seventh bit pops into the carry. But with ROL, the carry moves into the zeroth bit. A combination of ASL ROL shifts all the bits in a two-byte number to the left by 1:
||(our 1 before ASL low byte,
ROL high byte)
You can see that this, in effect, multiplies these bytes by 2. If we ASL/ROL again, we get:
||(the original number, mul-
tiplied by 4)
At this point, our answer is 4. We've multiplied the original 1 by 4 with an ASL/ROL combination, performed twice.
Now we CLC again and add the original number (1) to the current result (4), giving us 5 (460-520). It's easy to see that all we need to do now is one more ASL/ROL, which multiplies the running total by 2 one more time:
|(added to the original 1,
then, we just ASL the low byte:
ROL the high byte (which has no effect on this small a number):
||(giving us 10)
That final ASL/ROL multiplies 5 times 2, and we've got the right answer (530-540). This trick-multiply by 4, add the original number, multiply by 2-will work whenever you need to multiply a number by 10. Other combinations will multiply by other numbers. And as Valdec illustrates, you can calculate powers of 10 by just running the result through this TEN subroutine as often as necessary.
Program 5-1. Open1, Commodore
10 ; "OPEN1" OPEN 1,8,3,"WHATEVER NAME FROM SCREEN"
20 ; OPEN A FILE ON DISK (THIS TYPE OF FILE IS READ FROM)
40 OPEN1 JSR CLRCHN;RESTORE NORMAL I/0 (OUTPUT TO SCREEN, INPUT FROM KEYBOARD)
50 LDA #1; CLOSE DOWN DISK FILE CHANNEL #1
60 JSR CLOSE; (WE'RE GOING TO REOPEN IT NOW, BUT WE CLOSE IT FIRST)
70 LDA #1
80 STA FNUM; FILE#
90 LDA #8
100 STA FDEV; DEVICE NUMBER.
110 LDA #3
120 STA FSECOND; SECONDARY ADDR.
130 NAMEAD LDA #<FILEN; SET POINTER TO FILE NAME BUFFER (FILEN) IN LADS.
140 STA FNAMEPTR ;POINTER TO FILENAME ADDR.
150 LDA #>FILEN
160 STA FNAMEPTR+1
170 JSR OPEN; ROUTINE WITHIN BASIC THAT OPENS UP A NEW FILE
200 ; OPEN 2,8,2,"NAME" (OPENS DISK PROGRAM FILE FOR WRITING OBJECT CODE)
220 OPEN2 LDA #2; SEE DEFINITIONS ABOVE (SAME SETUP)
230 STA FNUM
240 LDA #8
250 STA FDEV
260 LDA #2
270 STA FSECOND
280 LDA #<FILEN
290 STA FNAMEPTR; POINTER TO FILENAME ADDR.
300 LDA #>FILEN
310 STA FNAMEPTR+1
320 JSR OPEN
330 JSR CLRCHN
360 ; OPEN 4,4 (OPENS FILE TO PRINTER)
380 OPEN4 LDA #4; SAME FORMAT, EXCEPT FNAMELEN
390 STA FNUM
400 LDA #4
410 STA FDEV
420 LDA #0; THERE IS NO FILE NAME SO SET FILENAME LENGTH TO ZERO.
430 STA FNAMELEN
440 JSR OPEN
450 JSR CLRCHN
480 ; LOAD "NAME" (LOADS A PROGRAM FILE, A SOURCE CODE FILE INTO RAM)
500 LOAD1 JSR CLRCHN;RESTORE NORMAL I/O
510 LDA #0
520 STA LOADFLAG; LOAD/VERIFY FLAG
530 STA ST; THE STATUS BYTE
540 LDA #8
550 STA FDEV; DEVICE NUMBER.
560 LDA #<FILEN; SET POINTER TO FILENAME BUFFER (FILEN) IN LADS.
570 STA FNAMEPTR ;POINTER TO FILENAME ADDR.
580 LDA #>FILEN
590 STA FNAMEPTR+1
600 JSR LOAD; ROUTINE WITHIN BASIC THAT LOADS IN A PROGRAM
610 JSR CLRCHN
615 LDA RAMSTART:STA PMEM:LDA RAMSTART+1:STA PMEM+1
630 .FILE FINDMN
Program 5-2. Open1, Apple
5 ;OPEN INPUT FILE
10 OPEN1 JSR CLRCHN
20 LDA #1: CLOSE FILE IF ALREADY OPEN
30 JSR CLOSE
40 LDA #<OPNREAD
50 STA FMOP
60 LDA #>OPNREAD
70 STA FMOP+1
80 JSR FMDRVRO
90 INC FOPEN1; SET INPUT FILE TO OPEN
105 ; OPEN OUTPUT FILE
110 OPEN2 LDA #<OPNWRIT
120 STA FMOP
130 LDA #>OPNWRIT
140 STA FMOP+1
150 JSR FMDRVRO
160 INC FOPEN2; SET OUTPUT FILE OPEN
180 OPEN4 RTS; OPEN NOT NEEDED TO PRINTER
185 ; READ ONE BYTE FROM INPUT FILE
190 RDBYTE LDA #<RDIB
200 STA FMOP
210 LDA #>RDlB
220 STA FMOP+1
230 JSR FMDRVR
240 JSR $3DC
250 STA PARM+1
260 STY FARM
270 LDY #08
280 LDA (PARM),Y; GET THE BYTE
295 ; WRITE ONE BYTE TO OUTPUT FILE
300 WRBYTE STA WRDATA
310 LDA #<WRlB
320 STA FMOP
330 LDA #>WRIB
340 STA FMOP+1
350 JSR FMDRVR
365 ; CLOSE INPUT FILE
370 CLOSE1 LDA FOPEN1; CHECK TO SEE IF INPUT FILE IS OPEN
380 BEQ CLOSE4; IF NOT EXIT
390 LDA #<CLOSER
400 STA FMOP
410 LDA #>CLOSER
420 STA FMOP+l
430 JSR FMDRVR
440 LDA #0
450 STA FOPEN1; SET INPUT FILE TO CLOSED
465 ; CLOSE OUTPUT FILE
470 CLOSE2 LDA FOPEN2; CHECK TO S=EE IF OUTPUT FILE IS OPEN
480 BEQ CLOSE4; IF NOT EXIT
490 LDA #<CLOSEW
500 STA FMOP
510 LDA #>CLOSEW
520 STA FMOP+l
530 JSR FMDRVR
540 LDA #0
550 STA FOPEN2; SET OUTPUT FILE TO CLOSED
570 CLOSE4 RTS; CLOSE NOT NEEDED FOR PRINTER
580 FMDRVRO LDY #08; PUT FILENAME INTO PARAMETER FIELD
590 LDA (FMOP),Y
600 STA FARM
620 LDA (FMOP),Y
630 STA FARM+1
640 LDA #<FILEN
650 STA TEMP
660 LDA #>FILEN
670 STA TEMP+1
680 LDY #00
690 LDA #$A0
700 PADFN STA (PARM),Y; FIRST FILL WITH SFACES
720 CPY #31
730 BNE PADFN
740 LDY #00
750 FMO LDA (TEMP),Y; THEN PUT FILENAME IN FARM
760 ORA #$80; MAKE SURE HIGH BIT SET
770 STA (PARM),Y
790 CPY FNAMELEN
800 BNE FMO
810 FMDRVR JSR $3DC; GET START ADDRESS TO PARAMETER FIELD
820 STA PARM+1
830 STY PARM
840 LDY #00
850 PARMSU LDA (FMOP),Y; PUT PARMS INTO PARM
860 STA (PARM),Y
880 CPY #18
890 BNE PARMSU
900 LDX #00
910 JSR $3D6; JSR TO FILE MANAGER IN DOS
925 ; SET CURRENT INPUT CHANNEL
930 CHKIN STX OPNI
945 ; SET CURRENT OUTPUT CHANNEL
950 CHKOUT TXA
960 STA OPNO
970 CPX #4; IF PRINTER THEN
980 BNE CHKOUTO
990 LDA #<PRNTRO; SET OUTPUT TO PRINTER
1000 STA CSWD
1010 LDA #>PRNTRO
1020 STA CSWD+1
1030 CHKOUTO RTS
1035 ; GET ONE BYTE FROM CURRENTLY OPEN CHANNEL
1040 CHARIN STY Y1
1050 STX X; SAVE X & Y REG
1060 LDA OPNI; CHECK TO SEE IF INPUT CHANNEL
1070 CMP #1
1080 BNE CTOUT; IF NOT EXIT
1090 JSR RDBYTE
1110 LDY Y1
1120 LDX X
1150 CTOUT LDY Y1
1165 ; OUTPUT ONE BYTE TO CURRENTLY OPEN CHANNEL
1170 PRINT STY Y1; SAVE REG
1180 STA A1
1190 LDA OPNO; CHECK TO SEE IF TO OUTPUT FILE
1200 CMP #02
1210 BNE NXT1
1220 LDA A1; YES, WRITE THE BYTE
1230 JSR WRBYTE
1240 JMP CTOUT
1250 PRNTRO STA A1; PRINTER OUTPUT ROUTINE
1260 CMP #$8D
1270 BNE PROUT
1280 LDA #10
1290 PROUT STA PRINR
1300 NOTDONE LDA PRNTRDN
1310 BMI NOTDONE
1320 LDA A1
1340 NXT1 LDA OPNO; CHECK TO SEE IF TO PRINTER
1350 CMP #4
1360 BNE NXT2
1370 LDA A1; YES, PRINT TO PRINTER
1380 JSR PRNTRO
1390 JMP CTOUT
1400 NXT2 LDA A1; NO, MUST BE TO SCREED:
1410 ORA #$80
1420 JSR COUT
1430 JMP CTOUT
1435 ; CLOSE ALL INPUT AND OUTPUT CHANNELS
1440 CLRCHN LDA #00
1450 STA OPNO
1460 STA OPNI
1470 LDA #$F0; RESET OUTPUT ROUTINE
1480 STA CSWD
1490 LDA #$FD
1500 STA CSWD+1
1515 ;CHECK FOR STOP KEY
1520 STOPKEY LDA $C000
1530 CMP #$83
1545 ; CLOSE OPEN FILES
1550 CLOSE CMP #01
1560 BNE CL2; CLOSE INPUT FILE?
1570 JMP CLOSE1
1580 CL2 CMP #02; NO, CLOSE OUTPUT FILE?
1590 BNE CL4
1600 JMP CLOSE2
1610 CL4 JMP CLOSE4; NO, MUST BE PRINTER
1700 ; BASIC WEDGE
1710 WEDGE STA A1
1720 LDA #$00; IS TXTPTR AT $200?
1730 CMP TXTPTR
1740 BNE OUT
1750 LDA #02
1760 CMP TXTPTR+1
1770 BNE OUT; NO, EXIT
1775 LDY #0
1780 NXTCHR LDA (TXTPTR),Y; IGNORE LEADING SPACES
1781 CMP #32
1782 BNE ISLNUM
1783 INC TXTPTR
1784 JMP NXTCHR
1790 ISLNUM CMP #$2F; IS IT A NUMBER?
1800 BCC OUT; NO, EXIT
1810 CMP #$3A
1820 BCC INSLIN
1830 OUT LDA $200; IS IT "ASM "?
1840 CMP #65
1850 BNE OUT1
1860 LDA $201
1870 CMP #83
1880 BNE OUT1
1890 LDA $202
1900 CMP #77
1910 BNE OUT1
1920 LDA $203
1930 CMP #32
1940 BNE OUT1; No. EXIT
1950 LDY #0; YES
1960 TFRNAM LDA $204,Y; TRANSFER NAME TO TOP OF SCREEN
1970 CMP #0
1980 BEQ ASM
1990 ORA #$80
2000 STA $400,Y
2020 JMP TFRNAM
2030 ASM LDA #$A0: PUT FOLLOWING 3 SPACES
2040 STA $400,Y
2050 STA $401,Y
2060 STA $402,Y
2070 PLA; PULL RETURN ADDRESS AND JUMP TO START
2090 JMP START
2100 OUT1 LDA Al; NORMAL CHRGET
2110 CMP #$3A
2120 BCS EXIT
2130 CMP #$20
2140 BNE NXT
2150 JMP CHRGET
2160 NXT SEC
2170 SBC #$30
2190 SBC #$D0
2200 EXIT RTS
2210 INSLIN LDX PRGEND; FOUND LINE NUMBER, NOW INSERT LINE
2220 STX VARTAB
2230 LDX PRGEND+1
2240 STX VARTAB+1
2260 JSR LINGET; GET LINE NUMBER
2270 JSR TOKNIZ
2300 JMP LININS; JUMP TO NORMAL INSERT LINE AND RESET LINE LINK ADDRESSES
2310 TOKNIZ LDY #00; TOKENIZE LINE
2320 STY HIGHDS
2330 LDA #02
2340 STA HIGHDS+1
2350 TK3 LDA (TXTPTR),Y
2360 STA (HIGHDS),Y
2380 CMP #00; END OF LINE
2390 BNE TK3
2400 DEY; YES
2410 TK4 DEY
2420 LDA (HIGHDS),Y; IGNORE FOLLOWING SPACES
2430 CMP #32
2440 BEQ TK4
2460 LDA #0
2470 STA (HIGHDS),Y
2520 INY; Y-REG HOLDS LINE LEPIGTH +6
2540 EDITSU LDA #<WEDGE; INITIALIZE WEDGE
2550 STA $BB
2560 LDA #>WEDGE
2570 STA $BC
2580 LDA #$4C; "JMP"
2590 STA $BA
2592 LDA #$FC:STA 115; SET HIMEM
2595 LDA #$79:STA 116
2610 .FILE FINDMN
Program 5-3. Open 1, Atari
100 OPEN1 JSR CLRCHN
110 LDA #1
120 JSR CLOSE
130 LDA #1
140 STA FNUM
150 LDA #4
160 STA FDEV
170 LDA #0
180 STA FSECOND
190 NAMEAD LDA #<FILEN
200 STA FNAMEPTR
210 LDA #>FILEN
220 STA FNAMEPTR+1
230 JSR OPEN
240 LDA ST
250 BMI OPENERR
260 LDA RAMFLAG
270 BEQ NOLOAD
280 JSR AFTEROPEN
290 LDA #<TEXTBAS
300 STA PMEM
310 LDA #>TEXTBAS
320 STA PMEM+1
330 NOLOAD RTS
340 OPENERR JSR ERRPRINT
350 JMP TOBASIC
360 OPEN2 LDA #2
370 STA FNUM
380 LDA #8
390 STA FDEV
400 LDA #0
410 STA FSECOND
420 LDA #<FILEN
430 STA FNAMEPTR
440 LDA #>FILEN
450 STA FNAMEPTR+1
460 LDA #2
470 JSR CLOSE
480 LDA ST
490 HMI OPENERR
500 JSR OPEN
510 LDX #2
520 JSR CHKOUT
530 LDA #255
540 JSR PRINT
550 JSR PRINT
560 LDA TA
570 JSR PRINT
580 LDA TA+1
590 JSR PRINT
600 LDA LLSA
610 JSR PRINT
620 LDA LLSA+1
630 JSR PRINT
640 JSR CLRCHN
660 OPEN4 LDA #4
670 STA FNUM
675 JSR CLOSE
680 LDA #8
690 STA FDEV
700 LDA #0
710 STA FSECOND
720 LDA #2
730 STA FNAMELEN
740 LDA #<PNAME
750 STA FNAMEPTR
760 LDA #>PNAME
770 STA FNAMEPTR+1
800 JSR OPEN
810 LDA ST
820 BMI OPENERR
830 JSR CLRCHN
850 PNAME .BYTE 80 58
860 .FILE D:FINDMN.SRC
Program 5-4. Findmn
10 ; "FINDMN" -- LOOKS THROUGH MNEMONICS FOR MATCH TO LABEL.
20 ; WE JMP TO THIS FROM EVAL. & JMP BACK TO 1 OF 2 LOCATIONS (JMP FOR SPEED)
30 FINDMN LDY #0
40 LDX #255; PREPARE X TO GO TO ZERO AT START OF LOOP
50 LOOP INX; X RAISED TO ZERO AT START OF LOOP
60 LDA MNEMONICS,Y; LOOK IN TABLE OF MNEMONICS
70 CMP LABEL; COMPARE IT TO 1ST CHAR. OF WORD IN LABEL BUFFER
80 BEQ MORE; IF =, COMPARE 2ND LETTERS OF TABLE VS. BUFFER
90 INY; OTHERWISE GO UP THREE IN THE TABLE TO FIND THE NEXT MNEMONIC
120 CPX #57; HAVE WE CHECKED ALL 56 MNEMONICS.
130 BNE LOOP; IF NOT, CONTINUE TRYING TO FIND A MATCH
140 NOMATCH JMP EQLABEL; DIDN'T FIND A MATCH (SO GO BACK TO EVAL)
150 MORE INY; COMPARE 2ND LETTER
160 LDA MNEMONICS,Y
170 CMP LABEL+1
180 BEQ MORE1; IF =, GO ON TO COMPARE 3RD AND FINAL LETTER
210 BNE LOOP; 2ND LETTER DIDN'T MATCH, TRY NEXT MNEMONIC (Y <> 0)
220 BEQ NOMATCH ; IF Y = 0, WE'VE GONE PAST TABLE (RETURN TO EVAL)
230 MORE1 INY; COMPARE 3RD LETTER
240 LDA MNEMONICS,Y
250 CMP LABEL+2
260 BEQ FOUND; IF 3RD LETTERS ARE =, WE'VE FOUND OUR MATCH
280 BNE LOOP; OTHERWISE TRY NEXT MNEMONIC
290 BEQ NOMATCH
300 FOUND LDA LABEL+3; THE 4TH CHAR. MUST BE A BLANK FOR THIS TO BE A MNEMONIC
310 CMP #32
320 BEQ FO1; IF SO, STORE DATA ABOUT THIS MNEMONIC & RETURN TO EVAL.
330 CMP #0; OR IF END OF LINE, IT WOULD BE AN IMPLIED ADDR. MNEMONIC LIKE INY
340 BNE NOMATCH; OTHERWISE, NO MATCH FOUND (IT'S NOT A MNEMONIC).
350 F01 LDA TYPES,X; STORE ADDR. TYPE.
360 STA TP
370 LDY OPS,X; STORE OPCODE
380 STY OP
390 END JMP EVAR; MATCH FOUND SO JUMP TO EVAR ROUTINE IN EVAL
400 .FILE GETSA
For the Atari version of Findmn, change line 400 to:
400 .FILE D:GETSA.SRC
Program 5-5. Getsa
10 ; "GETSA" GET STARTING ADDRESS FROM DISK (LEAVES DISK POINTING AT-
20 ; *= THIS SPACE (START ADDRESS)
30 ; (EXPECTS FILE #1 TO BE ALREADY OPENED).
40 ; -------------------------
50 GETSA LDX #1; SET UP INPUT CHANNEL FOR A DEVICE (TO GET BYTES)
60 JSR CHKIN; BASIC'S ROUTINE
70 LDX #6; WE NEED TO THROW AWAY THE 1ST 6 BYTES ON A DISK FILE (LINE LINK,
80 LSA JSR CHARIN; LINE #, AND 2 BYTES) (CHARIN IS "GET BYTE")
90 DEX; COUNT DOWN UNTIL WE'VE PULLED OFF THE
100 BNE LSA; 1ST 6 BYTES ... THEN------------------
110 JSR CHARIN; PULL IN NEXT BYTE
120 CMP #172; IS IT THE * SYMBOL
130 BEQ MSA; IF SO, GO BACK TO CALLER (EVAL SUBPROGRAM CALLS GETSA)
140 LDA #<MNOSTART; OTHERWISE, PRINT ERROR MESSAGE WHICH
150 STA TEMP; SAYS "NO START ADDRESS". POINT TO THIS ERROR MESSAGE IN
160 LDA #>MNOSTART; THE POINTER,"TEMP," AND PRINT THE MESSAGE (PRNTMESS)
170 STA TEMP+1; (NOTE: THIS NO-START-ADDRESS CONDITION OCCURS 2 WAYS: EITHER
180 JSR PRNTMESS; YOU FORGOT TO WRITE ONE OR YOU GAVE THE WRONG FILE NAME)
190 JMP FIN; GO BACK TO BASIC VIA THE SHUTDOWN ROUTINE WITHIN EVAL;-------
200 MSA RTS
210 .FILE VALDEC
217 ; -------------------------
220 ; "MEMSA" GET STARTING ADDRESS FROM MEMORY. LEAVES DISK POINTING AT-
230 ; *= THIS SPACE (START ADDRESS)
240 ; !! INITIALIZES PMEM TO START OF MEMORY
250 ; REPLACES "GETSA" SOURCE CODE FILE TO CREATE RAM-BASED ASSEMBLER.
260 ; -------------------------
270 MEMSA LDA RAMSTART:STA PMEM:LDA RAMSTART+1:STA PMEM+1
280 LDX #3:MEM1 JSR CHARIN:DEX:BNE MEM1; ADD 4 TO PMEM TO POINT TO *_
300 JSR CHARIN:CMP #172:BEQ MMSA
310 LDA #<MNOSTART:STA TEMP:LDA #>MNOSTART:STA TEMP+1:JSR PRNTMESS
320 JMP FIN; GO BACK TO BASIC VIA ROUTINE WITHIN EVAL
330 MMSA RTS
340 ; --------------------------
350 ; "NEW CHARIN" ASSEMBLE SOURCECODE FROM MEMORY RATHER THAN DISK.
360 ; (IMITATES CHARIN FOR DISK)
370 ; RETURNS WITH NEXT BYTE FROM MEMORY, IN A
380 ; --------------------------
390 CHARIN INC PMEM:BNE INCP1:INC PMEM+1; REPLACES CONVENTIONAL CHARIN/DISK
400 INCP1 STY Y:LDY #O:LDA (PMEM),Y:PHP:LDY Y:PLP:RTS; SAVE STATUS REGISTER
410 CHKIN RTS; REPLACES DISK ROUTINE IN DEFS
420 ; --------------------------
440 ; .. THE OTHER NECESSARY MODIFICATIONS ..
460 ;HERE ARE THE REST OF THE MODIFICATIONS WHICH CHANGE LADS FROM
470 ;DISK-BASED TO RAM-MEMORY-BASED SOURCE CODE ASSEMBLY:
500 ; 1. REMOVE DEFINITIONS OF CHARIN AND CHKIN (IN THE DEFS FILE)
510 ; (JUST INSERT A SEMICOLON AS THE 1ST CHARACTER
520 ; IN LINES 220 AND 240 OF THE DEFS SOURCE CODE FILE.)
540 ; 2. REPLACE "JSR GETSA" IN LINE 370 OF THE EVAL FILE WITH
550 ; "JSR MEMSA" AND REMOVE THE "JSR OPEN1" IN LINE 350 AND
560 ; LINE 4350 IN EVAL.
580 ; 3. PUT A ; IN FRONT OF ".FILE VALDEC" IN LINE 210 IN THIS FILE.
590 ; (IN OTHER WORDS, ALLOW THE NEW VERSIONS OF CHARIN ETC.
600 ; TO ASSEMBLE INTO THE FINISHED VERSION OF LADS.)
620 ; 4. PUT SEMICOLONS AS 1ST CHARACTER IN LINES 760,770,780, & 800
630 ; IN THE PSEUDO SUBPROGRAM. ALSO, CHANGE LINE 750 TO
640 ; READ "JSR LOAD1" (INSTEAD OF "JSR OPEN1").
660 .FILE VALDEC
Program 5-6. Getsa, Apple Modifications
To create the Apple version of Getsa, make the following changes and additions to Program 5-5:
75 LSA STX X
80 JSR CHARIN; RAM START ADDRESS, AND LINE LINK) (CHARINIS "GET BYTE")
85 LDX X
120 CMP #$2A; IS IT THE * SYMBOL
Program 5-7. Getsa, Atari Modifications
To create the Atari version of Getsa, omit lines 215-660 in Program 5-5 and change the following lines:
10 ;ATARI MODIFICATIONS--GETSA
50 GETSA LDA #<TEXTBAS:STA PMEM: LDA #>TEXTBA
55 LDX #1
70 JSR LINENUMBER
120 CMP #42
210 .FILE D:VALDEC.SRC
Program 5-8. Valdec
10 ; "VALDEC" TRANSLATE ASCII INPUT TO A TWO-BYTE INTEGER IN RESULT
20 ; SETUP/TEMP MUST POINT TO ASCII NUMBER (WHICH ENDS IN ZERO).
30 ; RESULTS/ RESULT HOLDS 2-BYTE RESULT
40 ; --------------------------
50 VALDEC LDY #0
55 ; READ ASCII FROM LEFT TO RIGHT--INCREMENTING Y --(TO FIND LENGTH)
60 VGETZERO LDA (TEMP),Y
70 BEQ VZERO; 0 DELIMITER FOUND
90 JMP VGETZERO;--------------- (FOR EXAMPLE, ASSUME ASCII IS "15")
110 VZERO STY VREND; SAVE LENGTH OF ASCII NUMBER (IN THE EXAMPLE, LEN = 2)
130 LDA #0; CLEAN "RESULT" VARIABLE (SET TO 0)
140 STA RESULT
150 STA RESULT+1
160 LDX #1; USE "X" VARIABLE AS A MULTIPLY-X10-HOW-MANY-TIMES COUNTER
170 STX X
180 VALLOOP LDA (TEMP),Y; LOAD IN THE RIGHTMOST ASCII CHARACTER (EX: "5")
190 AND #$0F; AS ASCII, 5 = $35. 0 STRIP OFF THE 3, LEAVING THE 5.
200 STA RADD; STORE IN MULTIPLICATION REGISTER
210 STA TSTORE; STORE IN "REMEMBER IT" REGISTER
220 LDA #0; PUT 0 IN BOTH THESE REGISTERS (IN THEIR HIGH BYTES)
230 STA RADD+1
240 STA TSTORE+1;----------------- MULTIPLY X10 AS MUCH AS NECESSARY------
250 VLOOP DEX; LOWER THE COUNTER. (IN THE EXAMPLE, X NOW = 0 FOR 1ST CHAR)
260 BEQ VGOON; SO WE DON'T JSR TO THE X10 SUBROUTINE IN THIS CASE)
270 JSR TEN; OTHERWISE,WE'D MULTIPLY THE NUMBER X10 AS MANY TIMES AS NECESSARY
280 LDA RADD; MOVE RESULT OF MULTIPLICATION INTO STORAGE REGISTER
290 STA TSTORE
300 LDA RADD+1
310 STA TSTORE+1; SAVING RESULTS OF MOST RECENT MULTIPLICATION
320 JMP VLOOP; CONTINUE MULTIPLYING X10 UNTIL X IS DOWN TO ZERO.--------
330 VGOON INC X; RAISE X BY 1 (SINCE WE'RE MOVING LEFT AND EACH NUMBER WILL
335 ; BE 10X THE ONE TO ITS RIGHT).
340 LDX X
350 JSR VALADD; ADD RADD TO RESULT (ADD IN RESULTS OF THE MULTIPLICATION)
360 DEY; MOVE INDEX OVER BY 1 (TO POINT TO NEXT ASCII CHAR. TO THE LEFT)
370 DEC VREND; LOWER LENGTH POINTER. IF IT'S NOT YET ZERO, THEN
380 BNE VALLOOP; CONTINUE PROCESSING THIS ASCII NUMBER
390 RTS; OTHERWISE RETURN TO CALLER.
400 ;--------------- MULTIPLY BY 10
410 TEN CLC
420 ASL RADD; MULTIPLY RADD X 4
430 ROL RADD+1
440 ASL RADD
450 ROL RADD+1;---------------
470 LDA TSTORE;PULL OUT ORIGINAL NUMBER AND ADD IT TO RESULT OF X4 (GIVING X5)
480 ADC RADD
490 STA RADD
500 LDA TSTORE+1
510 ADC RADD+1
520 STA RADD+1;------------ NOW, MULTIPLY X2. ((N*4+N)*2) IS N*10
530 ASL RADD
540 ROL RADD+1
560 ;------------- ADD RESULTS OF THE MULTIPLICATION TO THE INTEGER ANSWER
570 VALADD CLC
580 LDA RADD
590 ADC RESULT
600 STA RESULT
610 LDA RADD+1
620 ADC RESULT+1
630 STA RESULT+1
650 .FILE INDISK
Program 5-9. Valdec, Atari Modifications
To create the Atari version of Valdec, make the following changes and additions to Program 5-8:
10 ;ATARI MODIFICATIONS--VALDEC
61 CMP #48
62 BCC VZERO
63 CMP #58
70 BCS VZERO
650 .FILE D:INDISK.SRC
Return to Table of Contents | Previous Chapter | Next Chapter