John B. Matthews, M.D.
Return home.
Apple II Cross Development Notes.
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 | hd
000000: 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, wor
000020: 6c 64 21 00 ld!.
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_conf
Listing 3: ac.
$ cat ~/bin/ac
#/bin/sh
java -jar ~/bin/ac.jar $@
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.
/*
* MkObj: Convert 65c02.out to binary via System.out
* copyright (C) 2006 by John B. Matthews
* distribution per GPL
*/
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
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.");
}
}
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 output
changequote([,]) dnl rarely used characters for quotes
define([labelnum], 1) dnl initialize label counter
define([pshlabel], [ dnl create a new, unique label
pushdef([$1], [$1]labelnum) define([labelnum], incr(labelnum))]) dnl
define([poplabel], [popdef([$1])]) dnl return to a previous level
define(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+1
INCW:])
define(decw, [pshlabel([DECW])
;Decrement word $1^
LDA $1
BNE DECW
DEC $1+1
DECW 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 macros
PTR EQU $6
PRDEC EQU $ED24
PRNTAX EQU $F941
CROUT EQU $FD8E
COUT EQU $FDED
ORG $800
stadr($FFFF, PTR)
incw(PTR)
decw(PTR)
decw(PTR)
pradr(PTR)
prchr(=)
prdec(PTR)
JSR CROUT
RTS
Listing 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 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 steps to statically link Rod's Color Pattern with the lo-res tgi library are shown in Listing 8.
Listing 7: cc65: make hello.
HELLO=hello
...
${HELLO}: ${HELLO}.c
cl65 -O -t apple2enh ${HELLO}.c
ac -d p1.po ${HELLO}
ac -cc65 p1.po ${HELLO} bin < ${HELLO}
Listing 8: cc65: make rod.
ROD=rod
...
${ROD}: ${ROD}.c
co65 --code-label _a2e_lo_install a2e.lo.tgi
ca65 a2e.lo.s
cl65 -O -t apple2enh ${ROD}.c --obj a2e.lo.o
ac -d p1.po ${ROD}
ac -cc65 p1.po ${ROD} bin < ${ROD}
Now you have one less excuse 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 01-Jun-2008
Return home.