The number of subscribers keeps climbing! From 0 in September, to 45 in October, 85 in November, 179 on Christmas Eve, and now 242 in late January!
In This Issue...
Bug Reports
1. Several readers have reported a problem with the COPY program in the December issue. As written, if you try to copy a block of lines to a point before the first line of the program, the block is inserted between the first and second bytes of the first line. Ouch! To fix it, insert lines 2221-2225 and change line 2250:
2221 LDA A2L 2222 CMP A1L 2223 LDA A2H 2224 SBC A1H 2225 BCC .5 2250 .5 LDA SS MOVE IN SOURCE BLOCK |
2. When I typed up Lee Meador's article for the January issue, I inadvertently changed one address to a crazy value. The address $2746 in the 4th paragraph on page 9 should be $1246.
3. The Variable Cross Reference program for Applesoft from the November issue leaves something behind after it has run. If you LIST the Applesoft program after running VCR, the line number of the first line will come out garbage. This only happens the first time you use the LIST command. For some reason, typing CALL 1002 before the LIST will fix it. I haven't found out the cause or cure yet. If you find it first, let me know!
The Apple's built-in speaker is one of its most delightful features. To be sure, it is very limited; but I have used it for everything from sound effects in games to music in six parts (weird-sounding guitar chords) and even speech. Too many ways to put all in one AAL article! I will describe some of the sound effects I have used, and maybe you can go on from there.
The speaker hardware is very simple. A flip-flop controls the current through the speaker coil. Everytime you address $C030, the flip-flop changes state. This in turn reverses the current through the speaker coil. If the speaker cone was pulled in, it pops out; if it was out, it pulls in. If we "toggle" the state at just the right rate, we can make a square-wave sound. By changing the time between reversals dynamically, we can make very complex sounds. We have no control over the amplitude of the speaker motions, only the frequency.
Simple Tone: This program generates a tone burst of 128 cycles (or 256 half-cycles, or 256 pulses), with each half-cycle being 1288 Apple clocks. Just to make it easy, let's call Apple's clock 1MHz. It is really a little faster, but that will be close enough. So the tone will be about 388 Hertz (cycles per second, if you are as old as me!).
How did I figure out those numbers? To get the time for a half-cycle (which I am going to start calling a pulse), I added up the Apple 6502 cycles for each instruction in the loop. LDA SPEAKER takes 4 cycles. DEX is 2 cycles, and BNE is 3 cycles when it branches. The DEX-BNE pair will be executed 256 times for each pulse, but the last time BNE does not branch; BNE only takes 2 cycles when it does not branch. The DEY-BNE pair will branch during each pulse, so we use 5 cycles there. So the total is 4+256*5-1+5=1288 cycles. I got the frequency by the formula f=1/T; T is the time for a whole cycle, or 2576 microseconds.
1000 *--------------------------------- 1010 * SIMPLE TONE 1020 *--------------------------------- 1030 SPEAKER .EQ $C030 1040 *--------------------------------- 1050 TONE LDY #0 START CYCLE COUNTER 1060 LDX #0 START DELAY COUNTER 1070 .1 LDA SPEAKER TOGGLE SPEAKER 1080 .2 DEX DELAY LOOP 1090 BNE .2 1100 DEY QUIT AFTER 128 CYCLES 1110 BNE .1 1120 RTS |
Apple "Bell" Subroutine: Inside your monitor ROM there is a subroutine at $FBE2 which uses the speaker to make a bell-like sound. Here is a copy of that code. Notice that the pulse width is controlled by calling another monitor subroutine, WAIT.
1000 *--------------------------------- 1010 * APPLE "BELL" ROUTINE 1020 *--------------------------------- 1030 .OR $FBE2 IN MONITOR ROM 1040 .TA $800 1050 *--------------------------------- 1060 WAIT .EQ $FCA8 MONITOR DELAY ROUTINE 1070 SPEAKER .EQ $C030 1080 *--------------------------------- 1090 M.FBE2 LDY #192 # OF HALF-CYCLES 1100 BELL2 LDA #12 SET UP DELAY OF 500 MICROSECONDS 1110 JSR WAIT FOR A HALF CYCLE OF 1000 HERTZ 1120 LDA SPEAKER TOGGLE SPEAKER 1130 DEY COUNT THE HALF CYCLE 1140 BNE BELL2 NOT FINISHED 1150 RTS |
Machine-Gun Noise: What if we use a random pulse width? Then we get something called noise, instead of a tone. We can create a burst of pulses of random-sounding width by using values from some arbitrary place in the Apple's memory as loop counts. The program uses the 256 values starting at $BA00 (which is inside DOS). If you make just one burst like that, it doesn't sound like much. But if you make ten in a row, you get a pattern of repetitious random noise bursts that in this case sounds like machine-gun fire. Doesn't it? Well, close enough....
1000 *--------------------------------- 1010 * MACHINE-GUN NOISE 1020 *--------------------------------- 1030 SPEAKER .EQ $C030 1040 CNTR .EQ $00 1050 *--------------------------------- 1060 NOISE LDX #64 LENGTH OF NOISE BURST 1070 *--------------------------------- 1080 LDA #10 NUMBER OF NOISE BURSTS 1090 STA CNTR 1100 .2 LDA SPEAKER TOGGLE SPEAKER 1110 LDY $BA00,X GET PULSE WIDTH PSEUDO-RANDOMLY 1120 .1 DEY DELAY LOOP FOR PULSE WIDTH 1130 BNE .1 1140 DEX GET NEXT PULSE OF THIS NOISE BURST 1150 BNE .2 1160 DEC CNTR GET NEXT NOISE BURST 1170 BNE .2 1180 RTS RETURN |
Laser "SWOOP" Sound: We can change the pulse width by making it go from wide to narrow in steps of 5 microseconds. It sounds like a low tone that gradually slides higher and higher until it is beyond the range of the human ear (or the Apple speaker). I used this program in a "space war" game to go with the laser fire. Even though the sound was entirely generated before the laser even appeared on the screen, it looks and sounds like the light beam and sound are simultaneous.
I have indicated in line 1110 that you should try experimenting with some other values for the maximum pulse width count. I have included a separate entry point at SWOOP2 to make ten swoops in a row. Try the various values for the maximum width and run each one from SWOOP2. You might also experiment with running the pulse width in the opposite direction (from narrow to wide) by changing line 1200 to INC PULSE.WIDTH.
1000 *--------------------------------- 1010 * LASER "SWOOP" SOUND 1020 *--------------------------------- 1030 SPEAKER .EQ $C030 1040 PULSE.COUNT .EQ $00 1050 PULSE.WIDTH .EQ $01 1060 SWOOP.COUNT .EQ $02 1070 *--------------------------------- 1080 SWOOP LDA #1 ONE PULSE AT EACH WIDTH 1090 STA PULSE.COUNT 1100 LDA #160 START WITH MAXIMUM WIDTH 1110 * (ALSO TRY VALUES OF 40, 80, 128, AND 160.) 1120 STA PULSE.WIDTH 1130 .1 LDY PULSE.COUNT 1140 .2 LDA SPEAKER TOGGLE SPEAKER 1150 LDX PULSE.WIDTH 1160 .3 DEX DELAY LOOP FOR ONE PULSE 1170 BNE .3 1180 DEY LOOP FOR NUMBER OF PULSES 1190 BNE .2 AT EACH PULSE WIDTH 1200 DEC PULSE.WIDTH SHRINK PULSE WIDTH 1210 BNE .1 TO LIMIT OF ZERO 1220 RTS 1230 *--------------------------------- 1240 * MULTI-SWOOPER 1250 *--------------------------------- 1260 SWOOP2 LDA #10 NUMBER OF SWOOPS 1270 STA SWOOP.COUNT 1280 .1 JSR SWOOP 1290 DEC SWOOP.COUNT 1300 BNE .1 1310 RTS |
Another Laser Blast: This one sounds very much the same as the swoop of the previous program, but it uses less memory. You should try experimenting with the pulse widths of the first and last pulses in lines 1060 and 1130. You could also try changing the direction by substituting a DEX in line 1120.
1000 *--------------------------------- 1010 * ANOTHER LASER BLAST 1020 *--------------------------------- 1030 SPEAKER .EQ $C030 1040 *--------------------------------- 1050 BLAST LDY #10 NUMBER OF SHOTS 1060 .1 LDX #64 PULSE WIDTH OF FIRST PULSE 1070 .2 TXA START A PULSE WITHIN A SHOT 1090 .3 DEX DELAY FOR ONE PULSE 1100 BNE .3 1105 TAX 1110 LDA SPEAKER TOGGLE SPEAKER 1120 INX 1130 CPX #192 PULSE WIDTH OF LAST PULSE 1140 BNE .2 1150 DEY FINISHED SHOOTING? 1160 BNE .1 NO 1170 RTS |
Inch-Worm Sounds: I stumbled onto this one by accident, while looking for some sound effects for a lo-res graphics demo. The demo shows what is supposed to be an inch-worm, inching itself across the screen. By plugging various values (as indicated in lines 1100 and 1130), I got some sounds that synchronized beautifully with the animation. Complete with an exhausted sigh at the end!
1000 *--------------------------------- 1010 * INCH-WORM SOUNDS 1020 *--------------------------------- 1030 SPEAKER .EQ $C030 1040 PULSE.WIDTH .EQ $00 1050 PULSE.STEP .EQ $01 1060 PULSE.LIMIT .EQ $02 1070 *--------------------------------- 1080 INCH.WORM 1090 LDA #1 SET STEP TO 1 1100 * (ALSO TRY 77, 129, 179) 1110 STA PULSE.STEP 1120 LDA #176 SET PULSE.WIDTH AND LIMIT TO 176 1130 * (ALSO TRY 88) 1140 STA PULSE.WIDTH 1150 STA PULSE.LIMIT 1160 .1 LDA SPEAKER TOGGLE SPEAKER 1170 LDX PULSE.WIDTH DELAY LOOP FOR PULSE WIDTH 1180 .2 PHA LONGER DELAY LOOP 1190 PLA 1200 DEX END OF PULSE? 1210 BNE .2 NO 1220 CLC CHANGE PULSE WIDTH BY STEP 1230 LDA PULSE.WIDTH 1240 ADC PULSE.STEP 1250 STA PULSE.WIDTH 1260 CMP PULSE.LIMIT UNTIL IT REACHES THE LIMIT 1270 BNE .1 1280 RTS |
Touch-Tones Simulator: I used this one with a telephone demo program. The screen shows a touch tone pad. As you press digits on the keyboard, the corresponding button on the screen lights up (displays in inverse mode). Then the demo program CALLs this machine language code to produce the twin-tone sound that your telephone makes. It isn't perfect, you can't fool the Bell System. But it makes a good demo!
I will describe the program from the top down. The four variables in page zero are kept in a "safe" area, inside Applesoft's floating point accumulator. Applesoft doesn't use these locations while executing a CALLed machine language routine.
The Applesoft demo program stores the button number (0-9) in location $E7. This could be done with "POKE 231,DGT", but I had more fun using "SCALE=DGT". SCALE= is a hi-res graphics command, but all it really does is store the value as a one-byte integer in $E7. Since we aren't using hi-res graphics, the location is perfectly safe to use.
CALL 768 gets us to line 1150, TWO.TONES. This is the main routine. It uses the button number to select the two tone numbers from LOW.TONES and HIGH.TONES. ONE.TONE is called to play first the low tone, then the high tone, back and forth, for ten times each. This is my attempt to fool the ear, to make it sound like both are being played at once.
1000 *--------------------------------- 1010 * TOUCH TONES SIMULATOR 1020 *--------------------------------- 1030 SPEAKER .EQ $C030 1040 *--------------------------------- 1050 DOWNTIME .EQ $9D 1060 UPTIME .EQ $9E 1070 LENGTH .EQ $9F 1080 CHORD.TIME .EQ $A0 1090 *--------------------------------- 1100 BUTTON .EQ $E7 SET BY "SCALE= # " 1110 * USE VALUES FROM 0 THRU 9 1120 *--------------------------------- 1130 .OR $300 1140 *--------------------------------- 1150 TWO.TONES 1160 LDA #10 1170 STA CHORD.TIME 1180 .3 LDX BUTTON 1190 LDA LOW.TONES,X 1200 JSR ONE.TONE 1210 LDA HIGH.TONES,X 1220 JSR ONE.TONE 1230 DEC CHORD.TIME 1240 BNE .3 1250 RTS 1260 *--------------------------------- 1270 ONE.TONE 1280 TAY 1290 LDA DOWNTIME.TABLE,Y 1300 STA DOWNTIME 1310 LDA UPTIME.TABLE,Y 1320 STA UPTIME 1330 LDA LENGTH.TABLE,Y 1340 STA LENGTH 1350 *--------------------------------- 1360 PLAY LDY UPTIME 1370 LDA SPEAKER 1380 DEC LENGTH 1390 BEQ .4 FINISHED 1400 .1 DEY 1410 BNE .1 1420 BEQ .2 1430 .2 LDY DOWNTIME 1440 LDA SPEAKER 1450 DEC LENGTH 1460 BEQ .4 1470 .3 DEY 1480 BNE .3 1490 BEQ PLAY 1500 .4 RTS 1510 *--------------------------------- 1520 DOWNTIME.TABLE 1530 .HS 8E807468514942 1540 *--------------------------------- 1550 UPTIME.TABLE 1560 .HS 8E807469514942 1570 *--------------------------------- 1580 LENGTH.TABLE 1590 .HS 1412100F201D1A 1600 *--------------------------------- 1610 LOW.TONES 1620 .HS 03000000010101020202 1630 HIGH.TONES 1640 .HS 05040506040506040506 1650 *--------------------------------- 1660 * SIMULATED DRIVER 1670 *--------------------------------- 1680 MON.WAIT .EQ $FCA8 1690 PUNCH.ALL 1700 LDA #0 1710 STA BUTTON 1720 .1 JSR TWO.TONES 1730 LDA #0 1740 JSR MON.WAIT 1750 INC BUTTON 1760 LDA BUTTON 1770 CMP #10 1780 BCC .1 1790 RTS |
ONE.TONE wiggles the speaker for LENGTH half-cycles. Each half-cycle is controlled by either the UPTIME or DOWNTIME counts. These three parameters are selected from three tables, according to the tone number selected by TWO.TONES. Lines 1270-1340 pick up the values from the three tables and load the page zero variables. Lines 1360-1500 do the actual speaker motions and time everything. The purpose of having two routines, one for uptime and one for downtime, is to be able to more closely approximate the frequency. For example, if the loop count we ought to use is 104.5, we could use an uptime of 104 and a down time of 105; this makes the total time for the full cycle correct. The redundant BEQ in line 1420 is there to make the loop times for UPTIME and DOWNTIME exactly the same.
Since you do not have my Applesoft program, which drives this, I wrote a simulated drive to just "push" the buttons 0-9. Lines 1650-1790 do this. I separated each button push by a call to the monitor WAIT subroutine, to make them easier to distinguish.
1650 *------------------------------------ 1660 * SIMULATED DRIVER 1670 *------------------------------------ 1680 MON.WAIT .EQ $FCA8 1690 PUNCH.ALL 1700 LDA #0 1710 STA BUTTON 1720 .1 JSR TWO.TONES 1730 LDA #0 1740 JSR MON.WAIT 1750 INC BUTTON 1760 LDA BUTTON 1770 CMP #10 1780 BCC .1 1790 RTS |
Morse Code Output: I have always thought that computers really only need one output line and one input line for communicating with humans. I could talk to my Apple with a code key, and it could beep back at me. One of the first programs I attempted in 6502 language was a routine to echo characters in Morse code. I looked it up about two hours ago, and shuddered at my sloppy, inefficient, hard to follow code. So, I wrote a new one.
I broke the problem down into three littler ones: 1) getting the characters which are to be output; 2) converting the ASCII codes to the right number of dots and dashes; and 3) making tones and spaces of the right length.
SETUP.MORSE (lines 1190-1240) links my output routine through the monitor output vector. Line 1240 JMPs to $3EA to re-hook DOS after me.
MORSE (lines 1260-1310) are an output filter. If the character code is less than $B0, I don't know how to send it in Morse code; therefore, I just go to $FDF0 to finish the output on the screen. Codes exist for these other characters, but I did not look them up. If you want a complete routine, you should modify line 1260 to CMP #$A0 and add the extra codes to the code table (lines 1130-1170).
SEND.CHAR looks up the Morse code for the character in the code table, and splits it into the number of code elements (low-order three bits) and the code elements themselves (high-order five bits). If a code element is zero, a short beep (dot) is sounded. If an element is one, three calls to the short beep routine make one long beep (dash). Between elements, a silence equal to the length of a short beep intervenes. After the last beep of a character, a longer silence, equal to three short silences, is produced. A 00 code from the code table makes a silent gap of three times the inter-character gap.
EL.SPACE and EL.DIT are nearly identical. The only difference is that EL.DIT makes a sound by addressing the speaker, while EL.SPACE does not. The value of EL.PITCH determines the pulse width, and EL.SPEED determines the number of pulses for an inter-element-space or a short beep. If the code stream is too fast for you, you can slow it down by increasing either or both of these two numbers.
1000 *--------------------------------- 1010 * MORSE CODE OUTPUT 1020 *--------------------------------- 1030 SPEAKER .EQ $C030 1040 DUMMY .EQ $C000 1050 *--------------------------------- 1060 SAVEX .BS 1 1070 SAVEY .BS 1 1080 EL.COUNT .BS 1 1090 EL.CODE .BS 1 1100 EL.SPEED .EQ 120 1110 EL.PITCH .EQ 80 1120 *--------------------------------- 1130 CODES .HS FD7D3D1D0D0585C5E5F5 0, 1-9 1140 .HS 000000000000 1150 .HS 004284A4830124C3040274A344C2 @, A-M 1160 .HS 82E364D443038123146394B4C4 N-Z 1170 .HS 000000000000 1180 *--------------------------------- 1190 SETUP.MORSE 1200 LDA #MORSE 1210 STA $36 1220 LDA /MORSE 1230 STA $37 1240 JMP $3EA 1250 *--------------------------------- 1260 MORSE CMP #$B0 SEE IF PRINTING CHAR 1270 BCC .1 NO 1280 PHA SAVE CHAR ON STACK 1290 JSR SEND.CHAR 1300 PLA GET CHAR OFF STACK 1310 .1 JMP $FDF0 1320 *--------------------------------- 1330 SEND.CHAR 1340 STX SAVEX 1350 STY SAVEY 1360 SEC 1370 SBC #$B0 1380 TAX 1390 LDA CODES,X 1400 STA EL.CODE 1410 AND #7 GET ELEMENT COUNT 1420 BEQ .4 NO CODE 1430 STA EL.COUNT 1440 .1 ASL EL.CODE PUT NEXT ELEMENT INTO CARRY 1450 BCC .2 MAKE 'DIT' 1460 JSR EL.DIT MAKE 'DAH' FROM 3 DITS 1470 JSR EL.DIT 1480 .2 JSR EL.DIT MAKE 'DIT' 1490 JSR EL.SPACE 1500 DEC EL.COUNT 1510 BNE .1 1520 .3 JSR CH.SPACE 1530 LDX SAVEX 1540 LDY SAVEY 1550 RTS 1560 .4 JSR CH.SPACE 1570 JSR CH.SPACE 1580 JMP .3 1590 *--------------------------------- 1600 CH.SPACE 1610 JSR EL.SPACE 1620 JSR EL.SPACE 1630 EL.SPACE 1640 LDY #EL.SPEED 1650 .1 LDX #EL.PITCH 1660 LDA DUMMY 1670 .2 DEX 1680 BNE .2 1690 DEY 1700 BNE .1 1710 RTS 1720 *--------------------------------- 1730 EL.DIT LDY #EL.SPEED 1740 .1 LDX #EL.PITCH 1750 LDA SPEAKER 1760 .2 DEX 1770 BNE .2 1780 DEY 1790 BNE .1 1800 RTS |
Several users of Version 4.0 have asked for a way to defeat the protection mechanism, so that they can store object code directly into the language card. One customer has a EPROM burner which accepts code at $D000. He wants to let the assembler write it out there directly, even though he could use the .TA directive and later a monitor move command. Or, he could use the .TF directive, and a BLOAD into his EPROM.
For whatever reason, if you really want to do it, all you have to do is type the following patch just before you assemble: $1A25:EA EA. In case you want to put it back, or check before you patch, what should be there is B0 28.
Brooke Boering wrote an excellent article, "Multiplying on the 6502", in MICRO--The 6502 Journal, December, 1980, pages 71-74. If you are wondering how to do it, or you want a faster routine for a special application, look up that article.
Brooke begins by explaining and timing the multiply subroutine found in the old Apple Monitor ROM. The time to multiply two 16-bit values and get a 32-bit result varies from 935 to 1511 microseconds, depending on how many "1" bits are in the multiplier. He proceeds to modify that subroutine to cut the execution time by 40%!
Finally, he presents two limited versions which are still quite useful in some applications. His 8x16 multiply averages only 383 microseconds, and his 8x8 version averages 192 microseconds.
Here is the code for his 16x16 version, which averages 726 microseconds. It has the same setup as the routine in the Apple ROM. On entry, the multiplicand should be in AUXL,AUXH ($54,55); the multiplier should be in ACL,ACH ($50,51); whatever is in XTNDL,XTNDH ($52,53) will be added to the product. Normally, XTNDL and XTNDH should be cleared to zero before starting to multiply. However, I have used this routine to convert from decimal to binary; I put the next digit in XTNDL and clear XTNDH, and then multiply the previous result by ten. The "next digit" is automatically added to the product that way. (I have corrected the typographical error in the listing as published in MICRO.)
1000 *--------------------------------- 1010 * FASTER 16X16 MULTIPLY 1020 * BY BROOKE W. BOERING 1030 * NEARLY AS PUBLISHED IN MICRO--THE 6502 JOURNAL 1040 * PAGE 72, DECEMBER, 1980. 1050 *--------------------------------- 1060 ACL .EQ $50 1070 ACH .EQ $51 1080 XTNDL .EQ $52 1090 XTNDH .EQ $53 1100 AUXL .EQ $54 1110 AUXH .EQ $55 1120 *--------------------------------- 1130 RMUL LDY #16 16-BIT MULTIPLIER 1140 .1 LDA ACL (AC * AUX) + XTND 1150 LSR CHECK NEXT BIT OF MULTIPLIER 1160 BCC .2 IF ZERO, DON'T ADD MULTIPLICAND 1170 CLC ADD MULTIPLICAND TO PARTIAL PRODUCT 1180 LDA XTNDL 1190 ADC AUXL 1200 STA XTNDL 1210 LDA XTNDH 1220 ADC AUXH 1230 STA XTNDH 1240 .2 ROR XTNDH SHIFT PARTIAL PRODUCT 1250 ROR XTNDL 1260 ROR ACH 1270 ROR ACL 1280 DEY NEXT BIT 1290 BNE .1 UNTIL ALL 16 1300 RTS 1310 *--------------------------------- 1320 * TEST ROUTINE FOR MULTIPLY 1330 *--------------------------------- 1340 SETUP.Y 1350 LDA #$4C PUT "JMP TESTMPY" IN $358-35A 1360 STA $3F8 1370 LDA #TESTMPY 1380 STA $3F9 1390 LDA /TESTMPY 1400 STA $3FA 1410 RTS 1420 *--------------------------------- 1430 TESTMPY 1440 LDA $3C MOVE A1L,A1H TO ACL,ACH 1450 STA ACL 1460 LDA $3D 1470 STA ACH 1480 LDA $3E MOVE A2L,A2H TO AUXL,AUXH 1490 STA AUXL 1500 LDA $3F 1510 STA AUXH 1520 LDA $42 MOVE A4L,A4H TO XTNDL,XTNDH 1530 STA XTNDL 1540 LDA $43 1550 STA XTNDH 1560 JSR RMUL MULTIPLY 1570 LDA XTNDH PRINT 32-BIT RESULT 1580 JSR $FDDA 1590 LDA XTNDL 1600 JSR $FDDA 1610 LDA ACH 1620 JSR $FDDA 1630 LDA ACL 1640 JMP $FDDA |
I wrote a test routine for the multiply, so that I could check it out. After assembling the whole program, I typed "MGO SETUP.Y" to link the control-Y Monitor Command to my test routine. Control-Y will parse three 16-bit hexadecimal values this way: val1<val2.val3cY stores val1 in $42,$43; val2 in $3C,$3D; and val3 in $3E,$3F. ("cY" stands for control-Y.)
I define val1 to be the initial value for XTNDL,XTNDH; this should normally be zero. The two values to be multiplied are val2 and val3. After TESTMPY receives control from the control-Y processor, it moves the three values into the right locations for the multiply subroutine. Then JSR RMUL calls the multiply routine. The following lines (1570-1640) print the 32-bit result by calling a routine in the monitor ROM which prints a byte in hex from the A-register.
Practically every program rearranges data in some way. Many times you must sort alphanumeric data, and Applesoft makes this relatively easy. At the heart of most sort algorithms you will have to swap two items.
If the items are numbers, you might do it like this: T=A(I) : A(I)=A(J) : A(J)=T. If the items are in string variables, you might use this: T$=A$(I) : A$(I)=A$(J) : A$(J)=T.
Before long, Applesoft's wonderful string processor eats up all available memory and your program screeches to a halt with no warning. You think your computer died. Just about the time you reach for the power switch, it comes to life again (if you aren't too impatient!); the garbage collection procedure has found enough memory to continue processing. If only Applesoft had a command to swap the pointers of two strings, this wouldn't happen.
What are pointers? Look on page 137 of your Applesoft Reference Manual. The third column shows how string variables are stored in memory. Each string, whether a simple variable or an element of an array, is represented by three bytes: the first byte tells how many bytes are in the string value at this time; the other two bytes are the address of the first byte of the string value. The actual string value may be anywhere in memory. I am calling the three bytes which define a string a "pointer".
All right, how can we add a string swap command? The authors of Applesoft thoughtfully provided us with the "&" command; it allows us to add as many new commands to the language as we want. (Last month I showed you how to add a computed GOSUB command using the &.) We could make up our own swap command; perhaps something like &SWAP A$(I) WITH A$(J). However, to keep it a little simpler, I wrote it this way: &A$(I),A$(J).
The program is in two sections. The first part, called SETUP, simply sets up the &-vector at $3F5, $3F6, and $3F7. It stores a "JMP SWAP" instruction there. When Applesoft finds an ampersand (&) during execution, it will jump to $3F5; our JMP SWAP will start up the second section.
SWAP calls on two routines inside the Applesoft ROMs: PTRGET ($DFE3) and SCAN.COMMA ($DEBE). I found the addresses for these routines in the article "Applesoft Internal Entry Points", by John Crossley, pages 12-18 of the March/April 1980 issue of The Apple Orchard. I also have disassembled and commented the Applesoft ROMs, so I checked to see if there were any bad side effects. Both routines assume that Applesoft is about to read the next character of your program. PTRGET assumes you are sitting on the first character of a variable name. SCAN.COMMA hopes you are sitting on a comma.
SWAP merely calls PTRGET to get the address of the pointer for the first variable, check for an intervening comma, and then calls PTRGET again to get the pointer address for the second variable. Then lines 1350-1430 exchange the three bytes for the two pointers.
1000 *--------------------------------- 1010 * STRING SWAP FOR APPLESOFT 1020 * "BRUN B.STRING.SWAP" TO SET IT UP; 1030 * THEN "&A$,B$" MEANS SWAP A$ AND B$. 1040 *--------------------------------- 1050 .OR $300 1060 .TF B.STRING.SWAP 1070 *--------------------------------- 1080 AMPERSAND.VECTOR .EQ $3F5 1090 *--------------------------------- 1100 PTRGET .EQ $DFE3 SCAN FOR VARIABLE NAME, 1110 * SEARCH FOR ITS ADDRESS, 1120 * LEAVE ADDRESS IN $83,$84 1130 * AND A,Y 1140 *--------------------------------- 1150 SCAN.COMMA .EQ $DEBE IF NEXT CHARACTER IS 1160 * IS A COMMA, SCAN OVER 1170 * IT; IF NOT, SYNTAX ERROR. 1180 *--------------------------------- 1190 A.PNTR .EQ $85,86 1200 B.PNTR .EQ $83,84 1210 *--------------------------------- 1220 SETUP LDA #SWAP SET UP AMPERSAND VECTOR 1230 STA AMPERSAND.VECTOR+1 1240 LDA /SWAP 1250 STA AMPERSAND.VECTOR+2 1260 LDA #$4C JMP OPCODE 1270 STA AMPERSAND.VECTOR 1280 RTS 1290 *--------------------------------- 1300 SWAP JSR PTRGET GET POINTER TO FIRST STRING 1310 STA A.PNTR 1320 STY A.PNTR+1 1330 JSR SCAN.COMMA CHECK FOR COMMA 1340 JSR PTRGET 1350 LDY #2 PREPARE TO SWAP 3 BYTES 1360 .1 LDA (A.PNTR),Y 1370 PHA 1380 LDA (B.PNTR),Y 1390 STA (A.PNTR),Y 1400 PLA 1410 STA (B.PNTR),Y 1420 DEY NEXT BYTE 1430 BPL .1 1440 RTS RETURN |
How about a demonstration? I have a list of 20 names (all are subscribers to the Apple Assembly Line), and I want to sort them into alphabetical order. Since I am just writing this to demonstrate using the swap command, I will use one of the WORST sort algorithms: the bubble sort.
Line 100 clears the screen and prints a title line. Line 110 loads the swap program and calls SETUP at 768 ($0300). Line 120 reads in the 20 names from the DATA statement in line 130, and calls a subroutine at line 200 to print the names in a column.
Lines 150-170 are the bubble sort algorithm. If two names are out of order, they are swapped at the end of line 160. Line 180 prints the sorted list of names in a second column.
100 TEXT : HOME : PRINT "DEMO USE OF 'STRING SWAP' ROUTINE" 110 DIM A$(20): PRINT CHR$(4)"BLOAD B.STRING.SWAP": CALL 768 120 FOR I = 1 TO 20: READ A$(I): NEXT : P = 1: GOSUB 200 130 DATA AMES,BURKE,PUTNEY,LEE,LEVY,RAMSDELL,BISHOP,RANDALL, LANDSMAN,LEIPER,OSLISLO,KOVACS,MEADOR,KRIEGSMAN,MERCIER, WHITE,LEVY,BLACK,SCHORNAK,STITT 140 REM BUBBLE SORT 150 M = 20 160 M = M - 1:SW = 0: FOR I = 1 TO M: IF A$(I+1) < A$(I) THEN SW = 1: & A$(I+1),A$(I): REM SWAP 170 NEXT : IF SW THEN 160 180 P = 20: GOSUB 200: END 200 VTAB 3: FOR I = 1 TO 20: HTAB P: PRINT A$(I): NEXT : RETURN |
A couple of months back, I was reading the most recent copy of the Apple Assembly Line. I was horrified! For some time I had been working on a disassembler that would work with the S-C Assembler II. There before my very eyes were not one, but two, advertisements for the disassemblers that would do the same things mine would do, and in some areas they would do a better job. Sure, my disassembler would form the source statements, put in the labels, take care of non-6502 machine language bytes and it would even provide a cross-reference listing as it went along. But, my program forms the source directly in memory. So, you can't disassemble much more than around 1.5K at a time. Secondly, I hadn't gotten it to the point that you could specify various commands and disassemble code, tables, strings, 16-bit data, etc. all in one pass. I had a separate program to do each one.
After thinking about it for some time I decided to sell what I've got. But how could I compete with the other two disassemblers with more features? They are even relatively low priced.
I did come up with a solution. (As if you hadn't guessed that by now.) I am offering my disassembler with the S-C source code and comments. In fact, you will need to assemble the program yourself to get it to work. If you want to join the various modules to create the program I was working toward, you can do that. If you don't like the way I did some part of the program you are free to make whatever changes you like. If the code you want to disassemble lies in the same place in memory as the disassembler, then you just reassemble the program somewhere else. You ARE limited to the size of memory. The disassembler, the table of labels, the program you are disassembling and the source version will have to fit in memory. For large programs you should disassemble in 1K pieces. But, if you choose too large a piece, be prepared to re-boot your system.
Let me make it clear that this disassembler works correctly. I have used it on thousands of bytes of machine language files. It does not die without cause. The source code it produces can be saved and when reassembled, the result matches the original program.
If you choose to order this from me you will get a 13-sector disk with the S-C Assembler II source code for the various programs. You will not get a well-written manual and you will not be able to boot and go. But, where else can you get a working version of a disassembler for only $2500 with the source code?
Ask for Lee Meador's Disassembler. Enclose check or money order for $25.00 (add $5.00 if ouside North America.) Your disassembler source code will be mailed within a day or two of when we receive your order.
Lee Meador, Box 3621, Arlington, TX 76010
(817)469-6019