Much as I love developing for the Apple II, developing on the Apple II can be tedious. Fortunately, it is easy to do Apple II cross development on a modern operating system using 6502 cross-development tools tools like a65, cc65, the command-line version of AppleCommander, and your favorite emulator. The simple "Hello, world!" program in Listing 1 will serve as a starting point.
Listing 1: hello.txt.
1 0300 org $300 2 FD8E crout equ $FD8E 3 FDED cout equ $FDED 4 0300 20 8E FD jsr crout 5 0303 A2 00 ldx #0 6 0305 BD 16 03 print lda msg,x 7 0308 F0 08 beq prln 8 030A 09 80 ora #$80 9 030C 20 ED FD jsr cout 10 030F E8 inx 11 0310 D0 F3 bne print 12 0312 20 8E FD prln jsr crout 13 0315 60 rts 14 0316 48 65 6C msg .byte "Hello, world!",$0 0319 6C 6F 2C 031C 20 77 6F 031F 72 6C 64 0322 21 00 $ ac -g hello ~/kegs/disks/Dev.2mg | hd000000: 20 8e fd a2 00 bd 16 03 f0 08 09 80 20 ed fd e8 ........... ...000010: d0 f3 20 8e fd 60 48 65 6c 6c 6f 2c 20 77 6f 72 .. ..`Hello, wor000020: 6c 64 21 00 ld!.Using a65.
The makefile rule in Listing 2 shows a series of commands suitable for assembling and copying the example to an Apple II disk image. The a65 command assembles the source hello.s, sending a listing to hello.txt and writing the object code to a text file named 65c02.out. Because the output is text, the java command invokes the MkObj program (below) to convert the file to binary; the subsequent hd command does a hex dump for quick visual inspection. The first ac command removes any previous version of the object file. The second one actually copies the binary to the target disk. The ac command itself is just a short script that invokes the command-line version of AppleCommander, passing along any command-line parameters (Listing 3). Finally, the disk_conf file is updated to tell this particular emulator (kegs-osx) to re-read the revised disk image.
Listing 2: Makefile.
hello: hello.s a65 -n -o hello.s > hello.txt java MkObj > hello hd hello ac -d ~/kegs/disks/Dev.2mg hello cat hello | ac -p ~/kegs/disks/Dev.2mg hello bin 2048 touch ~/kegs/disk_confListing 3: ac.
$ cat ~/bin/ac #/bin/sh java -jar ~/bin/ac.jar $@ Converting object file formats.
The default output of a65 is a text file named 65c02.out. It is suitable for use with the EXEC command under DOS or ProDOS. The java program in Listing 4 reads the file 65c02.out, parses the ASCII hex bytes and writes the equivalent binary to standard output, where is can be conveniently directed to a file.
Listing 4: MkObj.java.
import java.io.BufferedInputStream;import java.io.File;import java.io.FileInputStream;import java.io.InputStream;import java.io.IOException;/* * MkObj: Convert 65c02.out to binary via System.out * copyright (C) 2006; distribution per GPL * @author John B. Matthews */public class MkObj { public static void main(String[] args) { try { if (args.length == 0) { Convert("65c02.out"); } else if (args.length == 1 && "-h".equalsIgnoreCase(args[0])) { help(); } else if (args.length > 1) { help(); } else { Convert(args[0]); } } catch (Exception ex) { ex.printStackTrace(); help(); } } private static void Convert(String inName) throws IOException { File inputFile = new File(inName); BufferedInputStream in = new BufferedInputStream( new FileInputStream(inputFile)); int value; value = skip(in, (int) ':'); //skip to the first byte while ((value = getByte(in)) != -1) { if (value > -1) { System.out.write(value); } } in.close(); System.out.flush(); } private static int skip(InputStream in, int to) throws IOException { int c; do { c = in.read(); } while (!(c == -1 || c == to)); return c; } private static int getByte(InputStream in) throws IOException { StringBuffer s = new StringBuffer(); int c = in.read(); if (c == -1) { return -1; } else if ((c == 10) || (c == 13)) { c = skip(in, (int) ':'); return -2; } else if (c == (int) ' ') { return -3; } else { s.append((char) c); s.append((char) in.read()); return hexStringToInt(s.toString()); } } private static int hexStringToInt(String s) { int i = 0; try { i = Integer.parseInt(s, 16); } catch (NumberFormatException e) { } return i; } private static void help() { System.err.println("MkObj: Convert a65 -o to binary on System.out"); System.err.println("java MkObj [-h] [filename] [> destination]"); System.err.println("if no filename, default to 65c02.out."); }}Adding macros.
While a65 is a fine assembler, it lacks macro capability. Fortunately, the GNU macro processor m4 (available on many platforms) can be used to solve the problem. Listing 5 is an m4 macro file named macro.m4. The command "m4 macro.m4 > macro.s" can be added to the makefile rule above. Invoking m4 expands the macros contained in that file to produce the source for the assembly program shown in Listing 6. With some care, the resulting source and listings can be made quite readable.
The macro pshlabel allows each invocation of a macro to have a unique series of globally accessible labels. The macro poplabel allows a macro to return to a previously generated label in the series. The macros stadr, incw, decw, pradr, prchr and prdec are typical 6502 macros; the code itself is mostly a series of macro invocations. Because m4 is quite general in its approach, it can be used in the back-end of a compiler or simply to write assembly language programs directly.
Listing 5: macros.m4.
divert(-1) dnl Don't include macros in outputchangequote([,]) dnl rarely used characters for quotesdefine([labelnum], 1) dnl initialize label counterdefine([pshlabel], [ dnl create a new, unique label pushdef([$1], [$1]labelnum) define([labelnum], incr(labelnum))]) dnldefine([poplabel], [popdef([$1])]) dnl return to a previous leveldefine(stadr, [;Store $1 in $2 LDA #$1%256 STA $2 LDA #$1/256 STA $2+1])define(incw, [pshlabel([INCW]);Increment word $1^ INC $1 BNE INCW INC $1+1INCW:])define(decw, [pshlabel([DECW]);Decrement word $1^ LDA $1 BNE DECW DEC $1+1DECW DEC $1])define(pradr, [;Print $1 as hex LDX $1 LDA $1+1 JSR PRNTAX])define(prdec, [;Print $1 as decimal LDX $1 LDA $1+1 JSR PRDEC])define(prchr, [;Print $1 as char LDA #'$1' ORA #%10000000 JSR COUT])divert(0)dnl;Test macrosPTR EQU $6PRDEC EQU $ED24PRNTAX EQU $F941CROUT EQU $FD8ECOUT EQU $FDED ORG $800 stadr($FFFF, PTR) incw(PTR) decw(PTR) decw(PTR) pradr(PTR) prchr(=) prdec(PTR) JSR CROUT RTSListing 6: macros.txt.
1 ;Test macros 2 0006 PTR EQU $6 3 ED24 PRDEC EQU $ED24 4 F941 PRNTAX EQU $F941 5 FD8E CROUT EQU $FD8E 6 FDED COUT EQU $FDED 7 0800 ORG $800 8 9 ;Store $FFFF in PTR 10 0800 A9 FF LDA #$FFFF%256 11 0802 85 06 STA PTR 12 0804 A9 FF LDA #$FFFF/256 13 0806 85 07 STA PTR+1 14 15 ;Increment word PTR^ 16 0808 E6 06 INC PTR 17 080A D0 02 BNE INCW1 18 080C E6 07 INC PTR+1 19 080E INCW1: 20 21 ;Decrement word PTR^ 22 080E A5 06 LDA PTR 23 0810 D0 02 BNE DECW2 24 0812 C6 07 DEC PTR+1 25 0814 C6 06 DECW2 DEC PTR 26 27 ;Decrement word PTR^ 28 0816 A5 06 LDA PTR 29 0818 D0 02 BNE DECW3 30 081A C6 07 DEC PTR+1 31 081C C6 06 DECW3 DEC PTR 32 33 ;Print PTR as hex 34 081E A6 06 LDX PTR 35 0820 A5 07 LDA PTR+1 36 0822 20 41 F9 JSR PRNTAX 37 38 ;Print = as char 39 0825 A9 3D LDA #'=' 40 0827 09 80 ORA #%10000000 41 0829 20 ED FD JSR COUT 42 43 ;Print PTR as decimal 44 082C A6 06 LDX PTR 45 082E A5 07 LDA PTR+1 46 0830 20 24 ED JSR PRDEC 47 0833 20 8E FD JSR CROUT 48 0836 60 RTS
Using cc65.
Using cc65 is easier than ever with AppleCommander's -cc65 command-line option, as shown in Listing 7. Note that the starting address is not required. The second example shows how to link the lo-res library statically using the co65 utility.
Listing 7: cc65.
HELLO=helloROD=rod...${HELLO}: ${HELLO}.c cl65 -O -t apple2enh ${HELLO}.c ac -d p1.po ${HELLO} ac -cc65 p1.po ${HELLO} bin < ${HELLO}${ROD}: ${ROD}.c co65 --code-label _a2e_lo_install a2e.lo.tgi ca65 a2e.lo.s cl65 -T -l -O -t apple2enh ${ROD}.c --obj a2e.lo.o ac -d p1.po ${ROD} ac -cc65 p1.po ${ROD} bin < ${ROD}Using Aztec C.
Visit Bill Buckel's Apple Oldies at the Aztec C Museum.
Now you have several fewer excuses for not writing Apple II programs!
Copyright 2006, 2008 John B. Matthews
Distribution permitted under the terms of the GPL: http://www.gnu.org/copyleft/gpl.html.
Last updated 7-Jan-2010