diff options
author | B. Watson <urchlay@slackware.uk> | 2022-08-29 16:11:13 -0400 |
---|---|---|
committer | B. Watson <urchlay@slackware.uk> | 2022-08-29 16:11:13 -0400 |
commit | e2ba8458a5cfdfacfaf103e7ba97d610afa6c970 (patch) | |
tree | cd665e602e6e2b636578a7d3d7894380605dafcc | |
download | bw-atari8-tools-e2ba8458a5cfdfacfaf103e7ba97d610afa6c970.tar.gz |
initial commit
-rw-r--r-- | .syms | 0 | ||||
-rw-r--r-- | Makefile | 166 | ||||
-rw-r--r-- | README | 79 | ||||
-rw-r--r-- | TODO | 19 | ||||
-rw-r--r-- | a8eol.1 | 482 | ||||
-rw-r--r-- | a8eol.c | 567 | ||||
-rw-r--r-- | a8eol.rst | 196 | ||||
-rwxr-xr-x | a8utf8 | 163 | ||||
-rw-r--r-- | a8utf8.1 | 108 | ||||
-rw-r--r-- | a8utf8.rst | 52 | ||||
-rw-r--r-- | asmwrapper.sh | 35 | ||||
-rw-r--r-- | atr2xfd.1 | 201 | ||||
-rw-r--r-- | atr2xfd.c | 244 | ||||
-rw-r--r-- | atr2xfd.rst | 109 | ||||
-rw-r--r-- | atrsize.1 | 216 | ||||
-rw-r--r-- | atrsize.c | 308 | ||||
-rw-r--r-- | atrsize.rst | 92 | ||||
-rw-r--r-- | axe.1 | 166 | ||||
-rw-r--r-- | axe.c | 197 | ||||
-rw-r--r-- | axe.h | 52 | ||||
-rw-r--r-- | axe.rst | 114 | ||||
-rw-r--r-- | axelib.c | 546 | ||||
-rw-r--r-- | blob2c.1 | 137 | ||||
-rw-r--r-- | blob2c.c | 117 | ||||
-rw-r--r-- | blob2c.rst | 85 | ||||
-rw-r--r-- | cart.c | 261 | ||||
-rw-r--r-- | cart.h | 55 | ||||
-rw-r--r-- | cart2xex.1 | 247 | ||||
-rw-r--r-- | cart2xex.c | 583 | ||||
-rw-r--r-- | cart2xex.rst | 203 | ||||
-rwxr-xr-x | dasm2atasm | 275 | ||||
-rw-r--r-- | dasm2atasm.1 | 244 | ||||
-rw-r--r-- | dasm2atasm.rst | 162 | ||||
-rw-r--r-- | equates.inc | 1384 | ||||
-rw-r--r-- | fenders.1 | 282 | ||||
-rw-r--r-- | fenders.bin | bin | 0 -> 384 bytes | |||
-rw-r--r-- | fenders.c | 418 | ||||
-rw-r--r-- | fenders.dasm | 570 | ||||
-rw-r--r-- | fenders.rst | 239 | ||||
-rw-r--r-- | fenders_bin.c | 54 | ||||
-rw-r--r-- | fenders_bin.h | 9 | ||||
-rw-r--r-- | fenders_offsets.h | 5 | ||||
-rw-r--r-- | fenders_offsets.pl | 6 | ||||
-rw-r--r-- | fendersdbl.bin | bin | 0 -> 640 bytes | |||
-rw-r--r-- | fendersdbl.dasm | 411 | ||||
-rw-r--r-- | fendersdbl_bin.c | 86 | ||||
-rw-r--r-- | fendersdbl_bin.h | 9 | ||||
-rw-r--r-- | fendersdbl_offsets.h | 5 | ||||
-rw-r--r-- | get_address.c | 22 | ||||
-rw-r--r-- | get_address.h | 2 | ||||
-rw-r--r-- | loadscreen.bin | bin | 0 -> 222 bytes | |||
-rw-r--r-- | loadscreen.dasm | 153 | ||||
-rw-r--r-- | loadscreen_bin.c | 34 | ||||
-rw-r--r-- | loadscreen_bin.h | 9 | ||||
-rw-r--r-- | manftr.rst | 30 | ||||
-rw-r--r-- | manhdr.rst | 7 | ||||
-rw-r--r-- | rom2cart.1 | 244 | ||||
-rw-r--r-- | rom2cart.c | 641 | ||||
-rw-r--r-- | rom2cart.rst | 202 | ||||
-rw-r--r-- | rstman.rst | 58 | ||||
-rw-r--r-- | test2 | 2 | ||||
-rw-r--r-- | testdata | 1 | ||||
-rw-r--r-- | testdata.orig | 1 | ||||
-rw-r--r-- | unmac65.1 | 392 | ||||
-rw-r--r-- | unmac65.c | 1041 | ||||
-rw-r--r-- | unmac65.rst | 324 | ||||
-rw-r--r-- | ver.rst | 1 | ||||
-rw-r--r-- | xex.c | 313 | ||||
-rw-r--r-- | xex.h | 117 | ||||
-rw-r--r-- | xexcat.1 | 181 | ||||
-rw-r--r-- | xexcat.c | 248 | ||||
-rw-r--r-- | xexcat.rst | 132 | ||||
-rw-r--r-- | xexsplit.1 | 195 | ||||
-rw-r--r-- | xexsplit.c | 123 | ||||
-rw-r--r-- | xexsplit.rst | 128 | ||||
-rw-r--r-- | xextest.c | 87 | ||||
-rw-r--r-- | xfd2atr.1 | 132 | ||||
-rw-r--r-- | xfd2atr.c | 227 | ||||
-rw-r--r-- | xfd2atr.rst | 82 |
79 files changed, 15088 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..82b4afc --- /dev/null +++ b/Makefile @@ -0,0 +1,166 @@ + +# Install paths. DESTDIR is used for installing to an alternate location, +# for people making RPM/deb/tgz/etc packages. +DESTDIR= +PREFIX=/usr/local +BINDIR=$(PREFIX)/bin +MANDIR=$(PREFIX)/share/man +MAN1DIR=$(MANDIR)/man1 +DOCDIR=$(PREFIX)/share/doc/bw-atari8-tools + +# Compiler stuff +CC=gcc +CFLAGS=-Wall -O2 -ansi -D_GNU_SOURCE -DVERSION=\"$(VERSION)\" + +# BINS and SCRIPTS go in $BINDIR, DOCS go in $DOCDIR +BINS=a8eol xfd2atr atr2xfd blob2c cart2xex fenders xexsplit xexcat atrsize rom2cart unmac65 axe +SCRIPTS=dasm2atasm a8utf8 +MANS=a8eol.1 xfd2atr.1 atr2xfd.1 blob2c.1 cart2xex.1 fenders.1 xexsplit.1 xexcat.1 atrsize.1 rom2cart.1 unmac65.1 axe.1 dasm2atasm.1 a8utf8.1 +DOCS=README equates.inc *.dasm + +# All the programs share this version number... +VERSION=0.2.1 + +# If your system doesn't support gzipped man pages, comment this out: +GZIP_MAN=y + +# unmac65 can be built for Atari 8-bit. Don't do it by default, but +# these variables are used for cross-compiling: +CC65=cl65 +CC65FLAGS=-O -t atari + +# Some distros have this with a .py extension, some don't. Only needed +# if you're rebuilding the man pages (users shouldn't have to). +# RST2MAN=rst2man.py +RST2MAN=rst2man + +# Targets below. You probably don't need to edit below this point. +# WARNING: Don't do a "make realclean" unless you have the DASM or +# Atasm 6502 cross assembler installed! +# "make clean" and "make distclean" will not delete the 6502 object +# code (the *.bin files), but "make realclean" will. + +all: $(BINS) manpages + +a8eol: a8eol.c + $(CC) $(CFLAGS) -o a8eol a8eol.c + +xfd2atr: xfd2atr.c + $(CC) $(CFLAGS) -o xfd2atr xfd2atr.c + +atr2xfd: atr2xfd.c + $(CC) $(CFLAGS) -o atr2xfd atr2xfd.c + +# note to cross-compiler users: If you're building the *.bin targets, +# blob2c needs to be executable on the build host. It'd also be nice +# to build a blob2c for the target platform... Probably you can do +# something like this: + +# make blob2c CC=/usr/bin/cc # build host blob2c +# make CC=/path/to/cross/cc # build everything else (uses blob2c) +# rm -f blob2c # get rid of host blob2c so we can... +# make blob2c CC=/path/to/cross/cc # build the target system's blob2c +# make install DESTDIR=/tmp/whatever... + +# Note that this is only needed if you're building the 6502 object code, +# which you don't need to do unless you've modified it (the distribution +# tarball comes with prebuilt *.bin files). + +blob2c: blob2c.c + $(CC) $(CFLAGS) -o blob2c blob2c.c + +fenders.bin: fenders.dasm asmwrapper.sh + sh asmwrapper.sh fenders + +fenders_bin.c: fenders.bin blob2c + ./blob2c fenders.bin > fenders_bin.c 2>fenders_bin.h + +fenders_offsets.h: fenders.bin fenders_offsets.pl + perl fenders_offsets.pl < fenders.syms > fenders_offsets.h + +fenders: fenders.c fenders_bin.c fenders_bin.h fenders_offsets.h \ + fendersdbl_bin.c fendersdbl_bin.h fendersdbl_offsets.h + $(CC) $(CFLAGS) -o fenders fenders.c fenders_bin.c fendersdbl_bin.c + +fendersdbl.bin: fendersdbl.dasm asmwrapper.sh + sh asmwrapper.sh fendersdbl + +fendersdbl_bin.c: fendersdbl.bin blob2c + ./blob2c fendersdbl.bin > fendersdbl_bin.c 2>fendersdbl_bin.h + +fendersdbl_offsets.h: fendersdbl.bin fenders_offsets.pl + perl fenders_offsets.pl < fendersdbl.syms > fendersdbl_offsets.h + +loadscreen.bin: loadscreen.dasm asmwrapper.sh + sh asmwrapper.sh loadscreen + +loadscreen_bin.c: loadscreen.bin blob2c + ./blob2c loadscreen.bin > loadscreen_bin.c 2>loadscreen_bin.h + +cart2xex: cart2xex.c loadscreen_bin.c get_address.o cart.o + $(CC) $(CFLAGS) -o cart2xex cart2xex.c loadscreen_bin.c get_address.o cart.o + +rom2cart: rom2cart.c cart.o + $(CC) $(CFLAGS) -o rom2cart rom2cart.c cart.o + +cart.o: cart.c cart.h + $(CC) $(CFLAGS) -c cart.c + +get_address.o: get_address.c get_address.h + $(CC) $(CFLAGS) -c get_address.c + +xex.o: xex.c xex.h + $(CC) $(CFLAGS) -c xex.c + +xextest: xextest.c xex.o + $(CC) $(CFLAGS) -o xextest xextest.c xex.o + +xexsplit: xexsplit.c xex.o + $(CC) $(CFLAGS) -o xexsplit xexsplit.c xex.o + +xexcat: xexcat.c xex.o get_address.o + $(CC) $(CFLAGS) -o xexcat xexcat.c xex.o get_address.o + +unmac65.xex: unmac65.c + @rm -f unmac65.o + $(CC65) $(CC65FLAGS) -DVERSION=\"$(VERSION)\" -DTAG=\"$(TAG)\" -t atari -o unmac65.xex unmac65.c + @rm -f unmac65.o + +axe: axe.c axe.h axelib.c + +manpages: $(MANS) + +%.1: %.rst + $(RST2MAN) $< > $@ + +# "make clean" does NOT remove the .bin or _bin.[ch] files. This is +# for people who don't have either dasm or atasm installed. +# also, it doesn't remove the man pages. these are checked into git, even. +clean: + rm -f core *.o *~ $(BINS) + +distclean: clean + rm -rf *.syms *.atr 1 2 3 *.xex *.rom *.atasm *.m65 atrcheck cart2rom + +realclean: distclean + rm -f *.bin *_bin.[ch] *_offsets.h *.1 + +install: all + mkdir -p $(DESTDIR)/$(BINDIR) $(DESTDIR)/$(MAN1DIR) $(DESTDIR)/$(DOCDIR) + strip $(BINS) + for i in $(BINS) $(SCRIPTS) ; do \ + install -m0755 -oroot -groot $$i $(DESTDIR)/$(BINDIR) ; \ + install -m0644 -oroot -groot $$i.1 $(DESTDIR)/$(MAN1DIR) ; \ + if [ "$(GZIP_MAN)" = "y" ]; then \ + gzip $(DESTDIR)/$(MAN1DIR)/$$i.1 ; \ + fi ; \ + done + ( cd $(DESTDIR)/$(BINDIR) && rm -f atrcheck && ln -s atr2xfd atrcheck ) + ( cd $(DESTDIR)/$(BINDIR) && rm -f cart2rom && ln -s rom2cart cart2rom ) + if [ "$(GZIP_MAN)" = "y" ]; then \ + cd $(DESTDIR)/$(MAN1DIR) && rm -f atrcheck.1.gz && ln -s atr2xfd.1.gz atrcheck.1.gz ; \ + cd $(DESTDIR)/$(MAN1DIR) && rm -f cart2rom.1.gz && ln -s rom2cart.1.gz cart2rom.1.gz ; \ + else \ + cd $(DESTDIR)/$(MAN1DIR) && rm -f cart2rom.1 && ln -s rom2cart.1 cart2rom.1 ; \ + fi + install -m0644 -oroot -groot $(DOCS) $(DESTDIR)/$(DOCDIR) @@ -0,0 +1,79 @@ +This is a collection of Atari 8-bit related utilities I've written for Linux. +They should be usable as-is on other UNIX-like systems, including Cygwin +for MS-Windows. + +a8eol - Convert Atari 8-bit text files to/from UNIX / DOS / Mac Classic + text file format. + +a8utf8 - Convert Atari 8-bit text to UTF-8 encoded Unicode. + +axe - ATR/XFD Editor. Copy files into & out of ATR and XFD images, + create blank ATR images, etc. + +atr2xfd - Convert an Atari 8-bit ATR disk image to a raw (XFD) image. + +atrcheck - Check an Atari 8-bit ATR disk image for various types of problems. + +atrsize - Change the size of an Atari 8-bit ATR disk image, or create + a blank ATR image. + +blob2c - Create C source and header files from a binary file + +cart2rom - Convert an Atari800 CART image to a raw ROM image. + +cart2xex - Convert an Atari 8-bit ROM cartridge image to a binary load + (XEX) file. + +dasm2atasm - Convert 6502 assembly in DASM syntax to ATASM (or MAC/65) format. + +fenders - Install Fenders 3-sector loader in boot sectors of an ATR image. + +rom2cart - Convert a raw Atari 8-bit cartridge ROM image to a CART + image for use with eumlators such as Atari800. + +unmac65 - Detokenize Atari 8-bit Mac/65 SAVEd files. + +xexcat - Concatenate Atari 8-bit executables (XEX) into a single XEX file. + +xexsplit - Split a multi-segment Atari 8-bit executable (XEX) into + multiple single-segment files. + +xfd2atr - Convert an Atari 8-bit XFD (raw) disk image to an ATR image. + +All are written in C, except a8utf8 and dasm2atasm which are written +in Perl. All utilities have man pages. + +Also included is "equates.inc", a 6502 assembly header file that defines +the Atari 8-bit system equates. It's meant to be used with either the +DASM or ATASM 6502 cross assemblers. + +To install, use the standard "make && make install" process. The default +prefix for installation is /usr/local. + +You may use "make install PREFIX=/somewhere/else" to install somewhere +other than /usr/local. Binaries will be installed to $PREFIX/bin, man +pages to $PREFIX/share/man/man1, and other documentation (including +equates.inc) to $PREFIX/share/doc/bw-atari8-tools. You also may use +BINDIR, MANDIR, MAN1DIR, and DOCDIR to explicitly set the installation +paths. Man pages are compressed with gzip by default. If your system +does not support gzipped man pages, try "make install GZIP_MAN=n". If +you're creating a distribution package (RPM, deb, Slackware tgz), +use "make install PREFIX=/usr DESTDIR=/tmp/whatever". This will +build everything for use in /usr, but actually install everything to +/tmp/whatever/usr, which can then be archived in whichever package +format you're using. + +blob2c is not actually Atari-specific: it could be useful for any project +where the contents of a file need to be compiled as an unsigned char +array in a C program. + +dasm2atasm is not Atari-specific, since the DASM and ATASM cross +assemblers can be used to develop code for any 6502-based platform +(though ATASM does have some nice extra features for the Atari 8-bit). +DASM supports several other CPUs besides the 6502, but dasm2atasm only +works with 6502 code. + +The latest version of bw-atari8-tools can always be found at +https://slackware.uk/~urchlay/repos/bw-atari8-tools + +-- B. Watson <urchlay@slackware.uk>; Urchlay on irc.libera.chat ##atari. @@ -0,0 +1,19 @@ +for now: +- convert man pages to RST (done) +- update email, website, etc in all docs (done?) +- update list of cart types in cart.c (done) +- document (and possibly enhance) a8utf8 (done) +- rename repo to bw-atari8-tools +- MANDIR => MAN1DIR in Makefile (done) +- clean up compiler warnings (mostly done, axe is still a mess) +- merge axe and unmac65 repos into this one (done). +- change license to WTFPL (done). +- fix axe, make it stop allocating an extra empty sector. + +later: +- add cassio? +- file magic? (finish it, see if file upstream wants it) +- new tool: a8grep. knows about xex files (but works on others), can + search for screen codes or inverse video, etc. also a8strings. +- new tool: something like ataricom or binload, but show checksums + of segments (so we can compare different versions of the same game). @@ -0,0 +1,482 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "A8EOL" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +a8eol \- convert Atari 8-bit text files to/from UNIX/Windows/Mac +.\" RST source for a8eol(1) man page. Convert with: +. +.\" rst2man.py a8eol.rst > a8eol.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fBa8eol\fP [\fI\-admu8tcpsxih\fP] [\fIinfile\fP] [\fIoutfile\fP] +.SH DESCRIPTION +.sp +\fBa8eol\fP converts between ATASCII and UNIX, MS\-DOS/Windows, and +Mac text file formats. It can auto\-detect the input file format +and set the output format accordingly, or the user can explicitly +set the input and output formats. Various options are available for +translating non\-printing characters, including a mode similar to what +old computer magazines used for program listings. +.SH OPTIONS +.SS File type options: +.INDENT 0.0 +.TP +.B \-a +Input is UNIX, MS\-DOS/Windows, or MacOS < 10 text; convert to Atari (EOL=$9B) +.TP +.B \-d +Input is Atari text; convert to MS\-DOS/Windows (EOL=$0A,$0D) +.TP +.B \-m +Input is Atari text; convert to MacOS < 10 (EOL=$0D) +.TP +.B \-u +Input is Atari text; convert to UNIX (EOL=$0A) +.UNINDENT +.sp +With none of the above: input type is auto\-detected; output type is +UNIX if input is Atari, or Atari if input is UNIX/DOS/Mac. +.sp +Only one file type option can be used per run of \fBa8eol\fP\&. If more than +one is given, the last one occurring on the command line will be used. +.SS Translation options: +.INDENT 0.0 +.TP +.B \-n +Translate EOL characters only; pass anything else as\-is (including +tabs and backspaces). +.TP +.B \-c +Replace non\-printing characters with ^x (ASCII input) or {x} +(ATASCII input). This option also enables the \fB\-8\fP option. When +the input file is ATASCII, the output resembles a program list‐ +ing from an old computer magazine (see \fBATASCII CODES\fP, below). +.TP +.B \-p +Replace non\-printing characters with \fI\&.\fP (period, dot). +.TP +.B \-s +Remove (strip) non\-printing characters. +.TP +.B \-x +Replace non\-printing characters with \fI\ex[hex]\fP\&. +.UNINDENT +.sp +With none of the above: EOL, tab, and backspace characters are +translated; everything else is passed through as\-is. +.sp +Only one translation option can be used per run of \fBa8eol\fP\&. If more than +one is given, the last one occurring on the command line will be used. +.SS Other options: +.INDENT 0.0 +.TP +.B \-8 +8\-bit ASCII/ATASCII mode: Do not strip bit 7 (inverse video). +This option may be used alone or combined with any of the +translation options, above. Characters with bit 7 set are +considered non\-printing. This option is always enabled when \fB\-c\fP +is used. +.TP +.B \-i +In\-place conversion. Original file renamed to \fIinfile~\fP\&. This option +can\(aqt be used when reading from standard input. +.TP +.B \-q +Quiet operation. Error messages will still be printed. +.TP +.B \-v +Verbose operation. Prints extra info about what \fBa8eol\fP is doing. +.TP +.B \-h +Print built\-in help message and exit. +.UNINDENT +.sp +Leave \fIinfile\fP blank or use \fB\-\fP to read from standard input (in which +case, don\(aqt use the \fB\-i\fP option). +.sp +Leave \fIoutfile\fP blank or use \fB\-\fP to write to standard output. +.SH NOTES +.sp +Without the \fB\-8\fP option, bit 7 is stripped (cleared) for all input +characters, \fIexcept\fP for the ATASCII EOL ($9B) character (when the +input is an Atari file). Bit 7 stripping occurs for each input +character \fIbefore\fP any of the translation options are applied. +.sp +The input type auto\-detection isn\(aqt perfect. It scans from the +beginning of the input, looking for either an ATASCII EOL or +an ASCII carriage return or linefeed. An Atari file with ATASCII +graphics may be mis\-detected as an ASCII file, if it contains +any $0A or $0D bytes before the first EOL ($0A and $0D are graphics +characters, in ATASCII). If this happens, force Atari input with +\fB\-d\fP, \fB\-m\fP, or \fB\-u\fP\&. +.sp +The auto\-detection also fails with an "Illegal seek" error when +reading from a pipe (e.g. \fBcat file | a8eol\fP). To avoid this, either +set the input type explicitly with one of \fB\-[admu]\fP, or read from a +regular file (possibly a temporary one created just for this purpose). +This is a bug (not a feature), but probably not worth the time +it\(aqd take to fix it. +.sp +The \fB\-a\fP option is "magical" in that it can handle input with UNIX +(\fI\en\fP), DOS (\fI\er\en\fP), or Mac Classic (\fI\er\fP only) line endings. In fact, it +can handle an input file containing any combination of the three line +ending types in the same file. +.SH ATASCII CODES +.sp +When the \fI\-c\fP option is used on ATASCII input, the special Atari +character codes are translated into human\-readable descriptive +strings. This is similar to the way old magazines (Compute!, Antic, +Analog) printed ATASCII codes in typeset program listings. +.sp +List of code translations: +.TS +center; +|l|l|l|l|l|. +_ +T{ +Dec +T} T{ +Hex +T} T{ +Keystroke(s) +T} T{ +Description +T} T{ +T} +_ +T{ +\-\- +T} T{ +\-\- +T} T{ +{inv} +T} T{ +Inverse Video (800: Atari Logo) +T} T{ +Start a sequence of inverse video characters +T} +_ +T{ +\-\- +T} T{ +\-\- +T} T{ +{norm} +T} T{ +Inverse Video (800: Atari Logo) +T} T{ +End a sequence of inverse video characters (back to normal) +T} +_ +T{ +0 +T} T{ +00 +T} T{ +{ctrl\-,} +T} T{ +Ctrl , +T} T{ +Heart; replaces ASCII NUL +T} +_ +T{ +27 +T} T{ +1B +T} T{ +{esc} +T} T{ +Esc Esc +T} T{ +Literal Escape character +T} +_ +T{ +28 +T} T{ +1C +T} T{ +{up} +T} T{ +Esc Ctrl \- +T} T{ +Cursor Up +T} +_ +T{ +29 +T} T{ +1D +T} T{ +{down} +T} T{ +Esc Ctrl = +T} T{ +Cursor Down +T} +_ +T{ +30 +T} T{ +1E +T} T{ +{left} +T} T{ +Esc Ctrl + +T} T{ +Cursor Left +T} +_ +T{ +31 +T} T{ +1F +T} T{ +{right} +T} T{ +Esc Ctrl * +T} T{ +Cursor Right +T} +_ +T{ +96 +T} T{ +60 +T} T{ +{ctrl\-.} +T} T{ +Ctrl . +T} T{ +Diamond; replaces ASCII grave accent: \(ga +T} +_ +T{ +123 +T} T{ +7B +T} T{ +{ctrl\-;} +T} T{ +Ctrl ; +T} T{ +Club; replaces ASCII left brace: { +T} +_ +T{ +125 +T} T{ +7D +T} T{ +{clear} +T} T{ +Esc Ctrl < or Esc Shift < +T} T{ +Clear screen (CLR/HOME on 800); Replaces ASCII right brace: } +T} +_ +T{ +126 +T} T{ +7E +T} T{ +{bksp} +T} T{ +Esc Backspace +T} T{ +Backspace (BACK S on 800); Replaces ASCII tilde: ~ +T} +_ +T{ +127 +T} T{ +7F +T} T{ +{tab} +T} T{ +Esc Tab +T} T{ +Tab to next tab stop; Replaces ASCII DEL: ~ +T} +_ +T{ +155 +T} T{ +9B +T} T{ +\-\- +T} T{ +Enter +T} T{ +Atari EOL (translated to \en, \er\en, or \er) +T} +_ +T{ +156 +T} T{ +9C +T} T{ +{del\-line} +T} T{ +Esc Shift BackSp +T} T{ +Delete logical line @ cursor +T} +_ +T{ +157 +T} T{ +9D +T} T{ +{ins\-line} +T} T{ +Esc Shift > +T} T{ +Insert blank line @ cursor +T} +_ +T{ +158 +T} T{ +9E +T} T{ +{clr\-tab} +T} T{ +Esc Ctrl Tab +T} T{ +Clear current tab stop +T} +_ +T{ +159 +T} T{ +9F +T} T{ +{set\-tab} +T} T{ +Esc Shift Tab +T} T{ +Set tab stop @ cursor position +T} +_ +T{ +253 +T} T{ +FD +T} T{ +{bell} +T} T{ +Esc Ctrl 2 +T} T{ +Ring bell (800: internal spkr) +T} +_ +T{ +254 +T} T{ +FE +T} T{ +{del\-char} +T} T{ +Esc Ctrl BackSp +T} T{ +Delete character @ cursor +T} +_ +T{ +255 +T} T{ +FF +T} T{ +{ins\-char} +T} T{ +Esc Ctrl > +T} T{ +Insert one space @ cursor +T} +_ +.TE +.sp +Other control characters are listed as \fI{ctrl\-X}\fP, where \fIX\fP is the +keystroke to use for entering the character. +.\" other sections we might want, uncomment as needed. +. +.\" FILES +. +.\" ===== +. +.\" ENVIRONMENT +. +.\" =========== +. +.SH EXIT STATUS +.sp +0 for success, non\-zero for any error. Error messages are printed to \fBstderr\fP\&. +.\" BUGS +. +.\" ==== +. +.\" EXAMPLES +. +.\" ======== +. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. @@ -0,0 +1,567 @@ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> + +#ifndef VERSION +#define VERSION "???" +#endif + +#define SELF "a8eol" + +#define BANNER \ + "a8eol v"VERSION" by B. Watson (WTFPL)\n" + +#define USAGE \ + BANNER \ + "Converts between Atari 8-bit and UNIX / DOS / Mac Classic text file format\n\n" \ + "Usage: a8eol -[admu8ncpsxih] [infile] [outfile]\n\n" \ + "File type options:\n" \ + " -a Input is UNIX, DOS, or MacOS < 10 text; convert to Atari (EOL=$9B)\n" \ + " -d Input is Atari text; convert to DOS (EOL=$0A,$0D)\n" \ + " -m Input is Atari text; convert to MacOS < 10 (EOL=$0D)\n" \ + " -u Input is Atari text; convert to UNIX (EOL=$0A)\n" \ + "With none of the above: input type is auto-detected; output type\n" \ + "is UNIX if input is Atari, or Atari if input is UNIX/DOS/Mac\n\n" \ + "Translation options:\n" \ + " -n Translate EOL characters only; pass anything else as-is\n" \ + " -c Replace non-printing characters with ^x or {x} (turns on -8, too)\n" \ + " -p Replace non-printing characters with '.'\n" \ + " -s Remove non-printing characters\n" \ + " -x Replace non-printing characters with \\x[hex]\n" \ + "With none of the above: EOL, tab, and backspace characters are\n" \ + "translated; everything else is passed through as-is.\n\n" \ + "Other options:\n" \ + " -8 8-bit ASCII/ATASCII mode: Do not strip bit 7 (inverse video).\n" \ + " -i 'In-place' conversion. Original file renamed to infile~\n" \ + " -q Quiet operation. Error messages will still be printed.\n" \ + " -v Verbose operation. Prints extra info about what a8eol is doing.\n" \ + " -h Print this help message\n\n" \ + "Leave infile blank or use '-' to read from standard input.\n" \ + "Leave outfile blank or use '-' to write to standard output.\n" + +#define OPTIONS "admu8ncpsxiqvh" + +#define FT_AUTO 0 +#define FT_ATARI 1 +#define FT_UNIX 2 +#define FT_DOS 3 /* input_type never gets set to this! */ +#define FT_MAC9 4 /* input_type never gets set to this! */ + +#define TT_NONE 0 +#define TT_CARET 1 +#define TT_DOT 2 +#define TT_HEX 3 +#define TT_STRIP 4 +#define TT_TABS 5 + +int input_type = FT_AUTO; /* FT_UNIX works for UNIX/DOS/Mac */ +int output_type = FT_AUTO; /* DOS/Mac need to be FT_DOS or FT_MAC9 */ +int trans_type = TT_TABS; +int keep_bit_7 = 0; +int in_place = 0; +int verbose = 1; +/* TODO: track bytes/lines read/written, print if verbose > 1 */ + +static int inverse = 0; +static char buf[50]; +static char *dot = "."; +static char *inv = "{inv}"; +static char *norm = "{norm}"; +static char *empty = ""; +static char *crlf = "\r\n"; +static char *cr = "\r"; +static char *lf = "\n"; +static char eol[2] = { 0x9b, '\0' }; + +/* FIXME: ata2asc() and asc2ata() are crap code. */ + +char *ata2asc(int c) { + char *modifier = empty; + static char result[50]; + int affects_inv = 1; + char c7 = c & 0x7f; + + if(c == 0x9b) { + switch(output_type) { + case FT_DOS: + return crlf; + + case FT_MAC9: + return cr; + + case FT_UNIX: + default: + return lf; + } + } + + if(!keep_bit_7) + c &= 0x7f; + + if(trans_type != TT_CARET && (c == '|' || (c >= 32 && c <= 122))) { + buf[0] = c; + buf[1] = '\0'; + return buf; + } + + if(trans_type == TT_DOT) { + return dot; + } else if(trans_type == TT_STRIP) { + return empty; + } else if(trans_type == TT_HEX) { + sprintf(buf, "\\x%02X", c); + return buf; + } else if(trans_type == TT_TABS) { + if(c == 127) { + buf[0] = '\t'; + buf[1] = '\0'; + return buf; + } else if(c == 126) { + buf[0] = '\b'; + buf[1] = '\0'; + return buf; + } else { + buf[0] = c; + buf[1] = '\0'; + return buf; + } + } + + if(c7 == '|' || (c7 >= 32 && c7 <= 122 && c7 != 96)) { + buf[0] = c7; + buf[1] = '\0'; + } else if(c7 == 0) { + sprintf(buf, "{ctrl-,}"); + } else if(c == 27) { + sprintf(buf, "{esc}"); + affects_inv = 0; + } else if(c == 28) { + sprintf(buf, "{up}"); + affects_inv = 0; + } else if(c == 29) { + sprintf(buf, "{down}"); + affects_inv = 0; + } else if(c == 30) { + sprintf(buf, "{left}"); + affects_inv = 0; + } else if(c == 31) { + sprintf(buf, "{right}"); + affects_inv = 0; + } else if(c7 == 96) { + sprintf(buf, "{ctrl-.}"); + } else if(c7 == 123) { + sprintf(buf, "{ctrl-;}"); + } else if(c == 125) { + sprintf(buf, "{clear}"); + affects_inv = 0; + } else if(c == 126) { + sprintf(buf, "{bksp}"); + affects_inv = 0; + } else if(c == 127) { + sprintf(buf, "{tab}"); + affects_inv = 0; + } else if(c == 156) { + sprintf(buf, "{del-line}"); + affects_inv = 0; + } else if(c == 157) { + sprintf(buf, "{ins-line}"); + affects_inv = 0; + } else if(c == 158) { + sprintf(buf, "{clr-tab}"); + affects_inv = 0; + } else if(c == 159) { + sprintf(buf, "{set-tab}"); + affects_inv = 0; + } else if(c == 253) { + sprintf(buf, "{bell}"); + affects_inv = 0; + } else if(c == 254) { + sprintf(buf, "{del-char}"); + affects_inv = 0; + } else if(c == 255) { + sprintf(buf, "{ins-char}"); + affects_inv = 0; + } else if(c7 < 32) { + sprintf(buf, "{ctrl-%c}", c7+64); + } + + if(affects_inv) { + if(c >= 128) { + if(!inverse) + modifier = inv; + + inverse = 1; + } else { + if(inverse) + modifier = norm; + + inverse = 0; + } + } + + + sprintf(result, "%s%s", modifier, buf); + return result; +} + +char *asc2ata(int c) { + if(c == '\n') { + return eol; + } + + if(!keep_bit_7) + c &= 0x7f; + + buf[0] = buf[1] = '\0'; + + if(trans_type == TT_NONE || c == '|' || (c >= 32 && c <= 122)) { + buf[0] = c; + return buf; + } + + if(trans_type == TT_DOT) { + return dot; + } else if(trans_type == TT_STRIP) { + return empty; + } else if(trans_type == TT_HEX) { + sprintf(buf, "\\x%02X", c); + return buf; + } + + /* TT_CARET and TT_TABS both translate tabs */ + if(c == '\t') { + buf[0] = 127; + return buf; + } else if(c == '\b') { + buf[0] = 126; + return buf; + } + + if(trans_type == TT_TABS) { + buf[0] = c; + return buf; + } + + /* handle TT_CARET */ + buf[0] = '^'; + buf[1] = '?'; + buf[2] = '\0'; + + if(c < 32) { + buf[1] = c + 64; + return buf; + } + + return buf; +} + +int main(int argc, char **argv) { + int c; + char *rename_to = NULL; + char *infile = NULL, *outfile = NULL; + FILE *in = NULL, *out = NULL; + int last = -1; + + /*** Parse args */ + while( (c = getopt(argc, argv, OPTIONS)) != -1 ) { + switch(c) { + case 'a': + input_type = FT_UNIX; + output_type = FT_ATARI; + break; + + case 'd': + input_type = FT_ATARI; + output_type = FT_DOS; + break; + + case 'm': + input_type = FT_ATARI; + output_type = FT_MAC9; + break; + + case 'u': + input_type = FT_ATARI; + output_type = FT_UNIX; + break; + + case '8': + keep_bit_7 = 1; + break; + + case 'n': + trans_type = TT_NONE; + break; + + case 'c': + trans_type = TT_CARET; + keep_bit_7 = 1; + break; + + case 'p': + trans_type = TT_DOT; + break; + + case 's': + trans_type = TT_STRIP; + break; + + case 'x': + trans_type = TT_HEX; + break; + + case 'i': + in_place = 1; + break; + + case 'q': + verbose = 0; + break; + + case 'v': + verbose++; + break; + + case 'h': + default: + printf(USAGE); + exit(1); + } + } + + /*** Get input filename, open input if not stdin */ + if(optind < argc) { + infile = argv[optind]; + if(strcmp(infile, "-") == 0) { + in = stdin; + } else if( !(in = fopen(infile, "rb")) ) { + fprintf(stderr, SELF ": (fatal) %s: %s\n", infile, strerror(errno)); + exit(1); + } + optind++; + } else { + in = stdin; + infile = "-"; + } + + if(in_place) { + /*** Setup in-place editing */ + int len; + + if(in == stdin) { + fprintf(stderr, + SELF ": (fatal) Can't do in-place edit of standard input. " + "Run '" SELF " -h' for help.\n"); + exit(1); + } + + /* Build backup filename */ + len = strlen(infile); + rename_to = (char *)malloc(len + 2); + if(!rename_to) { + fprintf(stderr, SELF ": (fatal) Out of memory\n"); + fclose(in); + exit(1); + } + + snprintf(rename_to, len + 2, "%s~", infile); + unlink(rename_to); + + /* Rename (link) input (it's already open, no problem) */ + if(link(infile, rename_to)) { + fprintf(stderr, SELF ": (fatal) can't create %s: %s\n", + rename_to, strerror(errno)); + fclose(in); + exit(1); + } + + if(verbose) + fprintf(stderr, SELF ": backed up '%s' as '%s'\n", infile, rename_to); + + unlink(infile); + + outfile = infile; + infile = rename_to; + } else if(optind < argc) { + /*** Get output filename */ + outfile = argv[optind]; + if(strcmp(outfile, "-") == 0) + out = stdout; + } else { + /*** No output filename, will write to stdout */ + out = stdout; + outfile = "-"; + } + + /*** Open output file, if not stdout */ + /* FIXME: if we *are* reading from stdin or writing to stdout on + DOS or Windows, how do we set binary mode on stdin/stdout? */ + if( out != stdout && !(out = fopen(outfile, "wb")) ) { + fprintf(stderr, SELF ": (fatal) %s: %s\n", outfile, strerror(errno)); + fclose(in); + exit(1); + } + + /*** Try not to confuse the newbie users, if we're reading from their + console (they may be expecting a help message) */ + if(verbose && in == stdin && isatty(fileno(in))) + fprintf(stderr, + SELF ": reading from standard input (run '" + SELF " -h' for help)...\n"); + + if(verbose > 1) { + /*** If requested, show the user what's about to happen */ + if(in_place) + fprintf(stderr, SELF ": Using in-place editing mode.\n"); + + fprintf(stderr, SELF ": Input file: '%s', type ", infile); + switch(input_type) { + case FT_AUTO: + fprintf(stderr, "will be auto-detected.\n"); + break; + + case FT_ATARI: + fprintf(stderr, "set to Atari.\n"); + break; + + case FT_UNIX: + fprintf(stderr, "set to UNIX/DOS/Mac\n"); + break; + } + + fprintf(stderr, SELF ": Output file: '%s', type ", outfile); + switch(output_type) { + case FT_AUTO: + fprintf(stderr, "will be auto-detected.\n"); + break; + + case FT_ATARI: + fprintf(stderr, "set to Atari.\n"); + break; + + case FT_UNIX: + fprintf(stderr, "set to UNIX.\n"); + break; + + case FT_DOS: + fprintf(stderr, "set to DOS.\n"); + break; + + case FT_MAC9: + fprintf(stderr, "set to Mac Classic.\n"); + break; + } + + fprintf(stderr, SELF ": Non-printable characters "); + switch(trans_type) { + case TT_NONE: + fprintf(stderr, "(incl. tabs/backspaces) will be passed as-is.\n"); + break; + + case TT_TABS: + fprintf(stderr, "will be passed as-is (tabs/backspaces will be translated).\n"); + break; + + case TT_CARET: + fprintf(stderr, "will be printed as ^x or {x}.\n"); + break; + + case TT_DOT: + fprintf(stderr, "will be printed as dots.\n"); + break; + + case TT_HEX: + fprintf(stderr, "will be printed as hex escapes.\n"); + break; + + case TT_STRIP: + fprintf(stderr, "will be stripped.\n"); + break; + } + + fprintf(stderr, SELF ": Bit 7 (inverse video) will be %s.\n", + (keep_bit_7 ? "passed as-is" : "stripped")); + } + + /*** Read input, process, write; lather, rinse, repeat */ + while(!feof(in)) { + int rew = 0; + c = getc(in); + if(c < 0) break; + + switch(input_type) { + /* Auto-detection works by reading the input until we find + an Atari EOL or a UNIX/DOS/Mac \n or \r, then rewinding + the stream. Will fail if reading from a pipe. */ + case FT_AUTO: + if(c == 0x9b) { + input_type = FT_ATARI; + output_type = FT_UNIX; + if(verbose) + fprintf(stderr, SELF ": input looks like an Atari file\n"); + rew++; + } else if(c == '\n' || c == '\r') { + input_type = FT_UNIX; + output_type = FT_ATARI; + if(verbose) + fprintf(stderr, SELF ": input looks like a UNIX/DOS/Mac file\n"); + rew++; + } + + /* rewind if possible */ + if(rew) { + if(fseek(in, 0L, SEEK_SET)) { + fprintf(stderr, + SELF ": (fatal) Can't seek in input: %s\n" + "Try again without type auto-detection.\n", + strerror(errno)); + fclose(in); + fclose(out); + exit(1); + } + continue; + } + break; + + case FT_ATARI: + fputs(ata2asc(c), out); + continue; + break; + + case FT_UNIX: + if(last == '\r' && c != '\n') { + /* Must be a Mac Classic text file... */ + putc(0x9b, out); + } else if(c == '\r') { + /* Swallow CR's */ + last = c; + continue; + } + + last = c; + fputs(asc2ata(c), out); + break; + } + } + + /* If the last CR was swallowed, spit it back out */ + if(input_type == FT_UNIX && last == '\r') + putc(0x9b, out); + + /*** All done, clean up. */ + fclose(in); + fclose(out); + + if(rename_to) + free(rename_to); + + if(input_type == FT_AUTO) { + fprintf(stderr, + SELF ": (fatal) Input didn't contain any EOL/CR/LF characters!\n"); + exit(1); + } + + return 0; +} diff --git a/a8eol.rst b/a8eol.rst new file mode 100644 index 0000000..ff81d82 --- /dev/null +++ b/a8eol.rst @@ -0,0 +1,196 @@ +.. RST source for a8eol(1) man page. Convert with: +.. rst2man.py a8eol.rst > a8eol.1 +.. rst2man.py comes from the SBo development/docutils package. + +===== +a8eol +===== + +------------------------------------------------------- +convert Atari 8-bit text files to/from UNIX/Windows/Mac +------------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +**a8eol** [*-admu8tcpsxih*] [*infile*] [*outfile*] + +DESCRIPTION +=========== + +**a8eol** converts between ATASCII and UNIX, MS-DOS/Windows, and +Mac text file formats. It can auto-detect the input file format +and set the output format accordingly, or the user can explicitly +set the input and output formats. Various options are available for +translating non-printing characters, including a mode similar to what +old computer magazines used for program listings. + +OPTIONS +======= + +File type options: +------------------ + +-a + Input is UNIX, MS-DOS/Windows, or MacOS < 10 text; convert to Atari (EOL=$9B) + +-d + Input is Atari text; convert to MS-DOS/Windows (EOL=$0A,$0D) + +-m + Input is Atari text; convert to MacOS < 10 (EOL=$0D) + +-u + Input is Atari text; convert to UNIX (EOL=$0A) + +With none of the above: input type is auto-detected; output type is +UNIX if input is Atari, or Atari if input is UNIX/DOS/Mac. + +Only one file type option can be used per run of **a8eol**. If more than +one is given, the last one occurring on the command line will be used. + +Translation options: +-------------------- + +-n + Translate EOL characters only; pass anything else as-is (including + tabs and backspaces). + +-c + Replace non-printing characters with ^x (ASCII input) or {x} + (ATASCII input). This option also enables the **-8** option. When + the input file is ATASCII, the output resembles a program list‐ + ing from an old computer magazine (see **ATASCII CODES**, below). + +-p + Replace non-printing characters with *.* (period, dot). + +-s + Remove (strip) non-printing characters. + +-x + Replace non-printing characters with *\\x[hex]*. + +With none of the above: EOL, tab, and backspace characters are +translated; everything else is passed through as-is. + +Only one translation option can be used per run of **a8eol**. If more than +one is given, the last one occurring on the command line will be used. + +Other options: +-------------- + +-8 + 8-bit ASCII/ATASCII mode: Do not strip bit 7 (inverse video). + This option may be used alone or combined with any of the + translation options, above. Characters with bit 7 set are + considered non-printing. This option is always enabled when **-c** + is used. + +-i + In-place conversion. Original file renamed to *infile~*. This option + can't be used when reading from standard input. + +-q + Quiet operation. Error messages will still be printed. + +-v + Verbose operation. Prints extra info about what **a8eol** is doing. + +-h + Print built-in help message and exit. + +Leave *infile* blank or use **-** to read from standard input (in which +case, don't use the **-i** option). + +Leave *outfile* blank or use **-** to write to standard output. + +NOTES +===== + +Without the **-8** option, bit 7 is stripped (cleared) for all input +characters, *except* for the ATASCII EOL ($9B) character (when the +input is an Atari file). Bit 7 stripping occurs for each input +character *before* any of the translation options are applied. + +The input type auto-detection isn't perfect. It scans from the +beginning of the input, looking for either an ATASCII EOL or +an ASCII carriage return or linefeed. An Atari file with ATASCII +graphics may be mis-detected as an ASCII file, if it contains +any $0A or $0D bytes before the first EOL ($0A and $0D are graphics +characters, in ATASCII). If this happens, force Atari input with +**-d**, **-m**, or **-u**. + +The auto-detection also fails with an "Illegal seek" error when +reading from a pipe (e.g. **cat file | a8eol**). To avoid this, either +set the input type explicitly with one of **-[admu]**, or read from a +regular file (possibly a temporary one created just for this purpose). +This is a bug (not a feature), but probably not worth the time +it'd take to fix it. + +The **-a** option is "magical" in that it can handle input with UNIX +(*\\n*), DOS (*\\r\\n*), or Mac Classic (*\\r* only) line endings. In fact, it +can handle an input file containing any combination of the three line +ending types in the same file. + +ATASCII CODES +============= + +When the *-c* option is used on ATASCII input, the special Atari +character codes are translated into human-readable descriptive +strings. This is similar to the way old magazines (Compute!, Antic, +Analog) printed ATASCII codes in typeset program listings. + +List of code translations: + +.. csv-table:: + :header: "Dec", "Hex", "Keystroke(s)", "Description" + + "--","--","{inv}","Inverse Video (800: Atari Logo)","Start a sequence of inverse video characters" + "--","--","{norm}","Inverse Video (800: Atari Logo)","End a sequence of inverse video characters (back to normal)" + "0","00","{ctrl-,}","Ctrl ,","Heart; replaces ASCII NUL" + "27","1B","{esc}","Esc Esc","Literal Escape character" + "28","1C","{up}","Esc Ctrl -","Cursor Up" + "29","1D","{down}","Esc Ctrl =","Cursor Down" + "30","1E","{left}","Esc Ctrl +","Cursor Left" + "31","1F","{right}","Esc Ctrl \*","Cursor Right" + "96","60","{ctrl-.}","Ctrl .","Diamond; replaces ASCII grave accent: \`" + "123","7B","{ctrl-;}","Ctrl ;","Club; replaces ASCII left brace: {" + "125","7D","{clear}","Esc Ctrl < or Esc Shift <","Clear screen (CLR/HOME on 800); Replaces ASCII right brace: }" + "126","7E","{bksp}","Esc Backspace","Backspace (BACK S on 800); Replaces ASCII tilde: ~" + "127","7F","{tab}","Esc Tab","Tab to next tab stop; Replaces ASCII DEL: ~" + "155","9B","--","Enter","Atari EOL (translated to \\n, \\r\\n, or \\r)" + "156","9C","{del-line}","Esc Shift BackSp","Delete logical line @ cursor" + "157","9D","{ins-line}","Esc Shift >","Insert blank line @ cursor" + "158","9E","{clr-tab}","Esc Ctrl Tab","Clear current tab stop" + "159","9F","{set-tab}","Esc Shift Tab","Set tab stop @ cursor position" + "253","FD","{bell}","Esc Ctrl 2","Ring bell (800: internal spkr)" + "254","FE","{del-char}","Esc Ctrl BackSp","Delete character @ cursor" + "255","FF","{ins-char}","Esc Ctrl >","Insert one space @ cursor" + +Other control characters are listed as *{ctrl-X}*, where *X* is the +keystroke to use for entering the character. + +.. other sections we might want, uncomment as needed. + +.. FILES +.. ===== + +.. ENVIRONMENT +.. =========== + +EXIT STATUS +=========== + +0 for success, non-zero for any error. Error messages are printed to **stderr**. + +.. BUGS +.. ==== + +.. EXAMPLES +.. ======== + +.. include:: manftr.rst + @@ -0,0 +1,163 @@ +#!/usr/bin/perl -w + +# convert A8 text to UTF-8. Control graphics characters are replaced with +# nearest Unicode equivalents (mostly from the box-drawing range, or from +# the basic-latin range with -i option). + +# Careful editing this script: you need an editor that groks UTF-8, or at +# least one that won't mangle the UTF-8 sequences embedded in the tables +# below. + +($SELF = $0) =~ s,.*/,,; + +binmode(STDOUT, ":utf8"); +binmode(STDIN, ":bytes"); + +use utf8; + +%atascii_table = ( + 0 => "♥", + 1 => "┣", + 2 => "┃", + 3 => "┛", + 4 => "┫", + 5 => "┓", + 6 => "╱", + 7 => "╲", + 8 => "◢", + 9 => "▗", + 10 => "◣", + 11 => "▝", + 12 => "▘", + 13 => "▔", + 14 => "▁", + 15 => "▖", + 16 => "♣", + 17 => "┏", + 18 => "━", + 19 => "╋", + 20 => "●", + 21 => "▄", + 22 => "▎", + 23 => "┳", + 24 => "┻", + 25 => "▌", + 26 => "┗", + 27 => "␛", + 28 => "↑", + 29 => "↓", + 30 => "←", + 31 => "→", + 96 => "◆", + 123 => "♠", + 125 => "↰", + 126 => "◀", + 127 => "▶", + 136 => "◤", + 137 => "▛", + 138 => "◥", + 139 => "▙", + 140 => "▟", + 141 => "▆", + 142 => "🮅", + 143 => "▜", + 148 => "◙", + 149 => "▀", + 150 => "🮊", + 153 => "▐", + 155 => "\n", + 156 => "⍐", + 157 => "⍗", + 158 => "⍇", + 159 => "⍈", + 160 => "█", +); + +%xl_intl_table = ( + 0 => "á", + 1 => "ù", + 2 => "Ñ", + 3 => "É", + 4 => "ç", + 5 => "ô", + 6 => "ò", + 7 => "ì", + 8 => "£", + 9 => "ï", + 10 => "ü", + 11 => "ä", + 12 => "Ö", + 13 => "ú", + 14 => "ó", + 15 => "ö", + 16 => "Ü", + 17 => "â", + 18 => "û", + 19 => "î", + 20 => "é", + 21 => "è", + 22 => "ñ", + 23 => "ê", + 24 => "ȧ", + 25 => "à", + 26 => "Ȧ", + 27 => "␛", + 28 => "↑", + 29 => "↓", + 30 => "←", + 31 => "→", + 96 => "¡", + 123 => "Ä", + 126 => "◀", + 127 => "▶", + 155 => "\n", +); + +undef $/; + +$table = \%atascii_table; +$print_table = 0; +while(@ARGV && $ARGV[0] =~ /^-/) { + for($ARGV[0]) { + /^-i$/ && do { $table = \%xl_intl_table; next; }; + /^-t$/ && do { $print_table = 1; next; }; + /^--?h/ && do { usage(0) }; + warn "$SELF: unknown option: $_\n"; + usage(1); + } + shift @ARGV; +} + +if($print_table) { + for(sort { $a <=> $b } keys %$table) { + my $chr = translate(chr $_); + $chr = '\n' if $chr eq "\n"; + printf '"%d","$%02x","%s"' . "\n", $_, $_, $chr; + } + exit 0; +} + +$_ = <>; +s/(.)/translate($1)/seg; +print; + +sub translate { + my $o = ord(shift); + my $ret; + + $ret = $table->{$o}; + return $ret if defined($ret); + + $ret = $table->{$o & 0x7f}; + return $ret if defined($ret); + + return chr($o & 0x7f); +} + +sub usage { + print <<EOF; +Usage: $SELF [--help] | [-t [-i]] | [-i] infile [infile ...] +See man page for details. +EOF + exit $_[0]; +} diff --git a/a8utf8.1 b/a8utf8.1 new file mode 100644 index 0000000..3ff998f --- /dev/null +++ b/a8utf8.1 @@ -0,0 +1,108 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "A8UTF8" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +a8utf8 \- Convert Atari 8-bit text to UTF-8 encoded Unicode. +.\" RST source for a8utf8(1) man page. Convert with: +. +.\" rst2man.py a8utf8.rst > a8utf8.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fIa8utf8\fP [\fB\-i\fP] [\fIinfile\fP] [\fIinfile ...\fP] +.sp +\fIa8utf8\fP [\fB\-i\fP] \fB\-t\fP +.SH DESCRIPTION +.sp +Convert Atari 8\-bit ATASCII or International Character Set text to +UTF\-8 encoded Unicode. Control graphics characters are replaced with +their nearest Unicode equivalents (mostly from the Box Drawing block, +or from the Basic Latin block with \fB\-i\fP option). +.sp +If no \fIinfile\fP is given, input is read from standard input. Output always +goes to standard output; to write to a file, use a command like: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +a8utf8 atari.txt > converted.txt +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +The output is plain UTF\-8 Unicode, without BOM. +.sp +Inverse video (characters codes above \fB$80\fP) are translated to +their non\-inverse equivalents, except \fB$9B\fP (Atari EOL), which is +translated to \fB\en\fP (newline). +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-i +Input uses Atari XL/XE International Character Set encoding, rather than +ATASCII graphics. +.TP +.B \-t +Print table of Atari to Unicode equivalents, in CSV format. Can +be used with or without \fB\-i\fP (two different tables). +.UNINDENT +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/a8utf8.rst b/a8utf8.rst new file mode 100644 index 0000000..2d36193 --- /dev/null +++ b/a8utf8.rst @@ -0,0 +1,52 @@ +.. RST source for a8utf8(1) man page. Convert with: +.. rst2man.py a8utf8.rst > a8utf8.1 +.. rst2man.py comes from the SBo development/docutils package. + +====== +a8utf8 +====== + +-------------------------------------------------- +Convert Atari 8-bit text to UTF-8 encoded Unicode. +-------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +*a8utf8* [**-i**] [*infile*] [*infile ...*] + +*a8utf8* [**-i**] **-t** + +DESCRIPTION +=========== + +Convert Atari 8-bit ATASCII or International Character Set text to +UTF-8 encoded Unicode. Control graphics characters are replaced with +their nearest Unicode equivalents (mostly from the Box Drawing block, +or from the Basic Latin block with **-i** option). + +If no *infile* is given, input is read from standard input. Output always +goes to standard output; to write to a file, use a command like:: + + a8utf8 atari.txt > converted.txt + +The output is plain UTF-8 Unicode, without BOM. + +Inverse video (characters codes above **$80**) are translated to +their non-inverse equivalents, except **$9B** (Atari EOL), which is +translated to **\\n** (newline). + +OPTIONS +======= + +-i + Input uses Atari XL/XE International Character Set encoding, rather than + ATASCII graphics. + +-t + Print table of Atari to Unicode equivalents, in CSV format. Can + be used with or without **-i** (two different tables). + +.. include:: manftr.rst diff --git a/asmwrapper.sh b/asmwrapper.sh new file mode 100644 index 0000000..56c8d9a --- /dev/null +++ b/asmwrapper.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +# Execute either the real dasm, or dasm2atasm + atasm. +# If we have dasm on the PATH, use it, otherwise fake it. +# dasm2atasm is NOT perfect! It does however work OK for +# fenders.dasm, fendersdbl.dasm, and loadscreen.dasm. + +# The .syms file format is totally different for dasm and atasm, but +# fenders_offsets.pl can handle either one (adapt for your purposes). + +# If you have dasm, but want to force atasm, pass any non-empty string +# as the second argument to this script. + +# Could also use "dasm2atasm -c" and ca65/ld65 to assemble, but I can't +# seem to get ca65 or ld65 to emit a symbol file or listing (the -l +# option doesn't work; the -m option doesn't do what I expect), so +# I don't know any way to extract the OFFSET_* labels from ca65's output. + +if [ -z "$1" ]; then + echo "$0: missing argument" + exit 1 +fi + +if [ -n "`which dasm 2>/dev/null`" -a -z "$2" ] ; then + exec dasm $1.dasm -f3 -s$1.syms -o$1.bin +elif [ -n "`which atasm 2>/dev/null`" ]; then + ln -sf equates.inc equates.m65 + perl dasm2atasm $1.dasm $1.atasm + exec atasm -r -s -o$1.bin $1.atasm > $1.syms +else + echo "$0: you need either dasm or atasm on your PATH" + exit 1 +fi + +exit 0 diff --git a/atr2xfd.1 b/atr2xfd.1 new file mode 100644 index 0000000..0680ea3 --- /dev/null +++ b/atr2xfd.1 @@ -0,0 +1,201 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "ATR2XFD" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +atr2xfd \- Convert an Atari 8-bit ATR disk image to a raw (XFD) image +.\" RST source for atr2xfd(1) man page. Convert with: +. +.\" rst2man.py atr2xfd.rst > atr2xfd.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fBatr2xfd\fP \fIinfile.atr\fP [\fIoutfile.xfd\fP] +.sp +\fBatrcheck\fP \fIinfile.atr\fP +.SH DESCRIPTION +.sp +\fBatr2xfd\fP strips the 16\-byte ATR header from an ATR image. While this +could be done with a command like: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +dd if=infile.atr outfile.xfd bs=16 skip=1 +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +\&...**atr2xfd** first checks that its input is in fact an ATR file, then +checks the ATR header and reports any problems it may find. +.sp +\fBatrcheck\fP performs the same checks as \fBatr2xfd\fP, but doesn\(aqt actually +write an XFD image. In fact, \fBatrcheck\fP is a symbolic link to \fBatr2xfd\fP, +which changes its behaviour (simply doesn\(aqt write any output) when +called via the link. +.sp +Neither \fBatr2xfd\fP nor \fBatrcheck\fP take any options. +.SH NOTES +.sp +For both commands, you may use \fB\-\fP for \fBinfile\fP to read from +standard input. For \fBatr2xfd\fP, use \fB\-\fP for \fBoutfile\fP to write +to standard output. If a filename is supplied for \fBoutfile\fP, it will +always be used as\-is (no \fI\&.xfd\fP extension will be appended). +.sp +If outfile is omitted, it is constructed like so: +.INDENT 0.0 +.IP \(bu 2 +If reading from standard input, write to standard output. +.IP \(bu 2 +If reading from a file whose name ends with an \fI\&.atr\fP or \fI\&.ATR\fP extension, +replace the extension with \fI\&.xfd\fP\&. +.IP \(bu 2 +Otherwise, append \fI\&.xfd\fP to the input filename. +.UNINDENT +.SH EXAMPLES +.sp +Check an image: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ atrcheck dos_20s.atr +atrcheck: size is 5760 16\-byte paragraphs +atrcheck: sectors: 720, sector size: 128 bytes +atrcheck: dos_20s.atr is a standard SS/SD image, 90K +atrcheck: ATR image OK (no fatal errors). +[ exit status is 0 ] +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Check an image and convert to XFD: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ atr2xfd dos_20s.atr +atr2xfd: input \(aqdos_20s.atr\(aq, output \(aqdos_20s.xfd\(aq +atr2xfd: size is 5760 16\-byte paragraphs +atr2xfd: sectors: 720, sector size: 128 bytes +atr2xfd: dos_20s.atr is a standard SS/SD image, 90K +atr2xfd: ATR image OK (no fatal errors). +atr2xfd: XFD image OK, wrote 92160 bytes +[ exit status is 0 ] +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Attempt to use atrcheck with an XFD image: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ atrcheck dos_20s.xfd +atrcheck: (fatal) dos_20s.xfd looks like an XFD image, not an ATR +[ exit status is 2 ] +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Here, games001.atr is one of the old Yogi/Jellystone 1 meg images: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ atrcheck games001.atr +atrcheck: size is 65536 16\-byte paragraphs +atrcheck: sectors: 8192, sector size: 128 bytes +atrcheck: games001.atr is a high\-capacity floppy or hard disk image, SD +atrcheck: ATR image OK (no fatal errors). +[ exit status is 0 ] +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +Here is an attempt to treat a non\-image file as an image: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ atrcheck /bin/ls +atrcheck: size is 5120 16\-byte paragraphs +atrcheck: (fatal) /bin/ls not an ATR file (no NICKATARI signature)! +[ exit status is 2 ] +.ft P +.fi +.UNINDENT +.UNINDENT +.SH EXIT STATUS +.sp +Exit status is zero for success, non\-zero for failure. Further, +exit status will be 1 for errors involving file I/O (file not found, +permissions, etc), and 2 for structural errors in the ATR file. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/atr2xfd.c b/atr2xfd.c new file mode 100644 index 0000000..c941266 --- /dev/null +++ b/atr2xfd.c @@ -0,0 +1,244 @@ +#include <stdio.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> + +#ifndef VERSION +#define VERSION "???" +#endif + +#define SELF "atr2xfd" +#define CHECK "atrcheck" + +int main(int argc, char **argv) { + char *type; + char *self = SELF; + struct stat st; + char infile[4096], outfile[4096]; + unsigned char buf[16]; + FILE *in, *out; + int i, paras, hparas, secsize, seccount; + int checkonly = 0, bytes = 0; + + if(strstr(argv[0], CHECK)) { + self = CHECK; + checkonly = 1; + } + + if(argc < 2 || argc > 3) { + fprintf(stderr, + "%s v" VERSION " by B. Watson (WTFPL)\n", self); + fprintf(stderr, + "Usage: %s input.xfd%s\n", + self, + (checkonly ? "" : " [output.atr]")); + exit(1); + } + + strcpy(infile, argv[1]); + if(argc == 3) { + strcpy(outfile, argv[2]); + if(checkonly) + fprintf(stderr, + "%s: output file not used with %s (ignoring it).\n", + self, self); + } else if(strcmp(infile, "-") == 0) { + strcpy(outfile, "-"); + } else { + char *p; + strcpy(outfile, argv[1]); + + p = strstr(outfile, ".atr"); + if(!p) p = strstr(outfile, ".ATR"); + if(!p) p = outfile + strlen(outfile); + strcpy(p, ".xfd"); + } + + if(!checkonly) + fprintf(stderr, "%s: input '%s', output '%s'\n", self, infile, outfile); + + if(strcmp(infile, "-") == 0) { + in = stdin; + } else { + if( !(in = fopen(infile, "rb")) ) { + fprintf(stderr, "%s: (fatal) can't read %s: %s\n", + self, infile, strerror(errno)); + exit(1); + } + } + + if(fstat(fileno(in), &st)) { + fprintf(stderr, "%s: (fatal) can't stat %s: %s\n", + self, infile, strerror(errno)); + exit(1); + } + + /* A few sanity checks... */ + if(st.st_size < 400) { + fprintf(stderr, + "%s: (fatal) %s too small to be an ATR image (<400 bytes)\n", + self, infile); + exit(2); + } + + if(st.st_size % 128 == 0 ) { + fprintf(stderr, + "%s: (fatal) %s looks like an XFD image, not an ATR\n", + self, infile); + exit(2); + } + + if( (st.st_size - 16) % 128 != 0 ) { + fprintf(stderr, + "%s: (fatal) %s not a valid ATR image (not an even number " + "of sectors)\n", + self, infile); + exit(2); + } + + if(st.st_size > (65535 * 256)) { + fprintf(stderr, + "%s: (fatal) %s too large to be an ATR image (>16M)\n", + self, infile); + exit(2); + } + + paras = st.st_size / 16 - 1; + fprintf(stderr, "%s: size is %d 16-byte paragraphs\n", self, paras); + + if(fread(buf, 1, 16, in) != 16) { + fprintf(stderr, + "%s: (fatal) can't read ATR header from %s: %s\n", + self, infile, strerror(errno)); + exit(1); + } + + if( !(buf[0] == 0x96 && buf[1] == 0x02) ) { + fprintf(stderr, + "%s: (fatal) %s not an ATR file (no NICKATARI signature)!\n", + self, infile); + exit(2); + } + + secsize = buf[4] + (buf[5] << 8); + + if( !(secsize == 128 || secsize == 256) ) { + fprintf(stderr, + "%s: (fatal) %s has invalid sector size %d\n", + self, infile, secsize); + exit(2); + } + + if(secsize == 128) + seccount = (st.st_size - 16) / secsize; + else { + seccount = (st.st_size - 400) / secsize + 3; + if((st.st_size - 16) % 256 != 128) + fprintf(stderr, "%s: partial sector at end of DD image, might " + "be a truncated or bogus ATR.\n", self); + } + + fprintf(stderr, + "%s: sectors: %d, sector size: %d bytes", + self, seccount, secsize); + + if(secsize == 256) + fprintf(stderr, " (first 3 sectors are 128 bytes)"); + fputc('\n', stderr); + + if(secsize == 128) { + if(seccount < 720) + type = "short SS/SD image, <90K"; + else if(seccount == 720) + type = "standard SS/SD image, 90K"; + else if(seccount == 1040) + type = "1050 SS/ED image, 130K"; + else if(seccount == 1440) + type = "XF551 DS/SD image, 180K"; + else + type = "high-capacity floppy or hard disk image, SD"; + } else { + if(seccount < 720) + type = "short SS/DD image, <180K"; + else if(seccount == 720) + type = "standard SS/DD image, 180K"; + else if(seccount == 1440) + type = "XF551 DS/DD or ATR8000 SS/QD image, 360K"; + else if(seccount == 2880) + type = "ATR8000 DS/QD or SS/PC image, 720K"; + else if(seccount == 5760) + type = "ATR8000 PC 1.44M image, 1440K"; + else + type = "high-capacity floppy or hard disk image, DD"; + } + + fprintf(stderr, "%s: %s is a %s\n", self, infile, type); + + fprintf(stderr, "%s: ATR image OK (no fatal errors).\n", self); + + hparas = buf[2] + (buf[3] << 8) + (buf[6] << 16); + if(hparas != paras) { + /* this is only a fatal error if checkonly is true */ + fprintf(stderr, + "%s: (%s) %s file size (%d paragraphs) doesn't agree with " + "ATR header (%d paragraphs). File may be truncated or corrupt.\n", + self, + (checkonly ? "fatal" : "warning"), + infile, paras, hparas); + + if(checkonly) + exit(1); + + fprintf(stderr, + "%s: Using actual file size for XFD image; expect trouble.\n", + self); + } + + if(checkonly) + exit(0); + + /* Only open the output file after the ATR is known to be good */ + if(strcmp(outfile, "-") == 0) { + out = stdout; + } else { + if( !(out = fopen(outfile, "wb")) ) { + fprintf(stderr, + "%s: (fatal) can't write %s: %s\n", + self, outfile, strerror(errno)); + exit(1); + } + } + + /* copy the data */ + while( (i = fgetc(in)) != EOF ) { + fputc(i, out); + bytes++; + } + + /* fputc() returns EOF on error *or* EOF; + check for I/O errors, return 1 if so */ + i = 0; + + if(ferror(in)) { + i = 1; + fprintf(stderr, + "%s: error reading %s: %s\n", self, infile, strerror(errno)); + } + + if(ferror(out)) { + i = 1; + fprintf(stderr, + "%s: error writing %s: %s\n", self, outfile, strerror(errno)); + } + + fclose(in); + fclose(out); + + if(!i) + fprintf(stderr, "%s: XFD image OK, wrote %d bytes\n", self, bytes); + + return i; +} diff --git a/atr2xfd.rst b/atr2xfd.rst new file mode 100644 index 0000000..fac2ed4 --- /dev/null +++ b/atr2xfd.rst @@ -0,0 +1,109 @@ +.. RST source for atr2xfd(1) man page. Convert with: +.. rst2man.py atr2xfd.rst > atr2xfd.1 +.. rst2man.py comes from the SBo development/docutils package. + +======= +atr2xfd +======= + +---------------------------------------------------------- +Convert an Atari 8-bit ATR disk image to a raw (XFD) image +---------------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +**atr2xfd** *infile.atr* [*outfile.xfd*] + +**atrcheck** *infile.atr* + +DESCRIPTION +=========== + +**atr2xfd** strips the 16-byte ATR header from an ATR image. While this +could be done with a command like:: + + dd if=infile.atr outfile.xfd bs=16 skip=1 + +...**atr2xfd** first checks that its input is in fact an ATR file, then +checks the ATR header and reports any problems it may find. + +**atrcheck** performs the same checks as **atr2xfd**, but doesn't actually +write an XFD image. In fact, **atrcheck** is a symbolic link to **atr2xfd**, +which changes its behaviour (simply doesn't write any output) when +called via the link. + +Neither **atr2xfd** nor **atrcheck** take any options. + +NOTES +===== + +For both commands, you may use **-** for **infile** to read from +standard input. For **atr2xfd**, use **-** for **outfile** to write +to standard output. If a filename is supplied for **outfile**, it will +always be used as-is (no *.xfd* extension will be appended). + +If outfile is omitted, it is constructed like so: + +- If reading from standard input, write to standard output. + +- If reading from a file whose name ends with an *.atr* or *.ATR* extension, + replace the extension with *.xfd*. + +- Otherwise, append *.xfd* to the input filename. + +EXAMPLES +======== + +Check an image:: + + $ atrcheck dos_20s.atr + atrcheck: size is 5760 16-byte paragraphs + atrcheck: sectors: 720, sector size: 128 bytes + atrcheck: dos_20s.atr is a standard SS/SD image, 90K + atrcheck: ATR image OK (no fatal errors). + [ exit status is 0 ] + +Check an image and convert to XFD:: + + $ atr2xfd dos_20s.atr + atr2xfd: input 'dos_20s.atr', output 'dos_20s.xfd' + atr2xfd: size is 5760 16-byte paragraphs + atr2xfd: sectors: 720, sector size: 128 bytes + atr2xfd: dos_20s.atr is a standard SS/SD image, 90K + atr2xfd: ATR image OK (no fatal errors). + atr2xfd: XFD image OK, wrote 92160 bytes + [ exit status is 0 ] + +Attempt to use atrcheck with an XFD image:: + + $ atrcheck dos_20s.xfd + atrcheck: (fatal) dos_20s.xfd looks like an XFD image, not an ATR + [ exit status is 2 ] + +Here, games001.atr is one of the old Yogi/Jellystone 1 meg images:: + + $ atrcheck games001.atr + atrcheck: size is 65536 16-byte paragraphs + atrcheck: sectors: 8192, sector size: 128 bytes + atrcheck: games001.atr is a high-capacity floppy or hard disk image, SD + atrcheck: ATR image OK (no fatal errors). + [ exit status is 0 ] + +Here is an attempt to treat a non-image file as an image:: + + $ atrcheck /bin/ls + atrcheck: size is 5120 16-byte paragraphs + atrcheck: (fatal) /bin/ls not an ATR file (no NICKATARI signature)! + [ exit status is 2 ] + +EXIT STATUS +=========== + +Exit status is zero for success, non-zero for failure. Further, +exit status will be 1 for errors involving file I/O (file not found, +permissions, etc), and 2 for structural errors in the ATR file. + +.. include:: manftr.rst diff --git a/atrsize.1 b/atrsize.1 new file mode 100644 index 0000000..3b9b571 --- /dev/null +++ b/atrsize.1 @@ -0,0 +1,216 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "ATRSIZE" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +atrsize \- Change the size of an Atari 8-bit ATR disk image, or create a blank ATR image +.\" RST source for atrsize(1) man page. Convert with: +. +.\" rst2man.py atrsize.rst > atrsize.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +atrsize [\fB\-bB\fP] \fIinfile.atr\fP [\fIsectors\fP] +.SH DESCRIPTION +.sp +Without the \fB\-b\fP or \fB\-B\fP options: +.INDENT 0.0 +.INDENT 3.5 +\fIinfile.atr\fP will be backed up to \fIinfile.atr~\fP, and a new \fIinfile.atr\fP will +be created. If \fIsectors\fP is given, the new image file will be truncated +or extended to the new size. Without \fIsectors\fP, the new image\(aqs size will +be set as follows: +.TS +center; +|l|l|l|. +_ +T{ +Density +T} T{ +Original Sectors +T} T{ +New Sectors +T} +_ +T{ +Either +T} T{ +0 \- 2 +T} T{ +Error +T} +_ +T{ +Either +T} T{ +3 \- 719 +T} T{ +720 +T} +_ +T{ +Either +T} T{ +720 +T} T{ +720 (no change) +T} +_ +T{ +Single +T} T{ +721 \- 1039 +T} T{ +1040 (aka 1050 enhanced density) +T} +_ +T{ +Single +T} T{ +1040 +T} T{ +1040 (no change) +T} +_ +T{ +Single +T} T{ +1041 or more +T} T{ +Unknown (must specify size) +T} +_ +T{ +Double +T} T{ +721 \- 1339 +T} T{ +1440 (aka double sided, double density) +T} +_ +T{ +Double +T} T{ +1440 +T} T{ +1440 (no change) +T} +_ +T{ +Double +T} T{ +1441 or more +T} T{ +Unknown (must specify size) +T} +_ +.TE +.sp +When \fIsectors\fP is given, its allowed range is from 3 to 65535. \fIinfile.atr\fP +will be rewritten at the new size. +.sp +When \fBatrsize\fP changes the size of an image, the new ATR header +will reflect the new size. If the new image is larger than the old +image, \fBatrsize\fP pads the image with empty sectors containing all 0 +data bytes. If the new image is smaller than the old image, it is +truncated, and any data in the old image that resides in the removed +sectors will be lost. +.sp +For ATR images where the ATR header doesn\(aqt agree with the actual size +of the file, the actual file size is used to determine the number of +sectors. The output image will have its ATR header adjusted to reflect +the actual file size of the image, if sectors is not given. +.UNINDENT +.UNINDENT +.sp +With \fB\-b\fP or \fB\-B\fP: +.INDENT 0.0 +.INDENT 3.5 +\fBatrsize\fP will create a new, blank image called \fIinfile.atr\fP\&. If +this file already exists, however, it will not be overwritten +(instead, \fBatrsize\fP will exit with a "file exists" message). +.sp +If \fIsectors\fP is given, the new image\(aqs size will be set to that many +sectors. If not given, the new image\(aqs size will be 720 sectors. +.sp +\fB\-b\fP creates a new image with 128\-byte sectors (single density) +.sp +\fB\-B\fP creates a new image with 256\-byte sectors (double density). +.sp +New images created with \fBatrsize\fP consist of a valid ATR header, +and sectors filled with zeroes. No boot sectors, directory, VTOC, +or filesystem are created. To use a blank image for file +storage, it must be formatted with an Atari DOS (either in an +emulator or with a real Atari via SIO2PC cable). If you\(aqre +trying to create a blank DOS 2.0S disk, use \fBaxe \-b\fP\&. +.UNINDENT +.UNINDENT +.SH NOTES +.INDENT 0.0 +.IP \(bu 2 +\fBatrsize\fP cannot change the sector size (density) of an image under +any circumstances. Only the sector count may be changed. +.IP \(bu 2 +\fBatrsize\fP will fail if the input file doesn\(aqt have a valid ATR header. +If it\(aqs an XFD (raw) image, use \fBxfd2atr\fP(1) to convert it to an ATR first. +.UNINDENT +.SH EXIT STATUS +.sp +Exit status is zero for success, non\-zero for failure. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/atrsize.c b/atrsize.c new file mode 100644 index 0000000..5571a02 --- /dev/null +++ b/atrsize.c @@ -0,0 +1,308 @@ +#include <stdio.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> + +/* FIXME: This code is terrible. + I should have written an ATR library for atrsize, atr2xfd, and xfd2atr + to use, instead of the copy/paste mess I've created. */ + +#ifndef VERSION +#define VERSION "???" +#endif + +#define SELF "atrsize" +#define USAGE \ + SELF " v" VERSION " by B. Watson (WTFPL)\n" \ + "Usage: " SELF " -[bB] image.atr [sectors]\n" \ + " -b Create image.atr as a new, blank, single-density image\n" \ + " -B Create image.atr as a new, blank, double-density image\n" \ + "Original image.atr is backed up as image.atr~\n" \ + "Without [sectors], size is rounded up to next 'standard' size\n" + +char *usage = USAGE; + +int main(int argc, char **argv) { + struct stat st; + int filesize = 0; + int headersize = 0; + int blank = 0; + int len, sec; + unsigned int secsize, seccount, paras, newseccount = 0, last_data_sec = 0; + char *newsize, infile[4096], *outfile; + unsigned char buf[384]; + FILE *in, *out; + + if(argc < 2 || argc > 4) { + fprintf(stderr, usage); + exit(1); + } + + if(strcmp(argv[1], "-b") == 0) { + blank = 128; + argv++; + argc--; + } else if(strcmp(argv[1], "-B") == 0) { + blank = 256; + argv++; + argc--; + } + + newsize = argv[2]; + + outfile = argv[1]; + strcpy(infile, outfile); + if( !(in = fopen(infile, "rb")) ) { + if(!blank) { + fprintf(stderr, + SELF ": %s: %s\n", infile, strerror(errno)); + exit(1); + } + + fprintf(stderr, + SELF ": %s does not exist, creating blank image\n", infile); + + secsize = blank; + seccount = 720; + + memset(buf, 0, 16); + + buf[0] = 0x96; + buf[1] = 0x02; + buf[4] = secsize & 0xff; + buf[5] = (secsize >> 8) & 0xff; + + filesize = headersize = 128 * 720 + (secsize - 128) * 717; + newseccount = seccount = 720; + + paras = headersize / 16; + buf[2] = paras & 0xff; + buf[3] = (paras >> 8) & 0xff; + buf[6] = (paras >> 16) & 0xff; + } + + if(in) { + if(blank) { + fprintf(stderr, + SELF ": won't create blank image %s: file exists\n", infile); + exit(1); + } + + if(fstat(fileno(in), &st)) { + fprintf(stderr, "Can't determine size of %s: %s\n", + infile, strerror(errno)); + exit(1); + } else { + filesize = st.st_size - 16; + if(filesize < 384) { + fprintf(stderr, + SELF ": %s is too small to be a valid ATR image\n", + infile); + exit(1); + } + } + + if(fread(buf, 1, 16, in) != 16) { + fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno)); + exit(1); + } + } + + if( !(buf[0] == 0x96 && buf[1] == 0x02) ) { + fprintf(stderr, + SELF ": (fatal) %s not an ATR file (no NICKATARI signature)!\n", + infile); + exit(2); + } + + secsize = buf[4] + (buf[5] << 8); + + if( !(secsize == 128 || secsize == 256) ) { + fprintf(stderr, + SELF ": (fatal) %s has invalid sector size %d\n", + infile, secsize); + exit(2); + } + + paras = buf[2] + (buf[3] << 8) + (buf[6] << 16); + headersize = paras * 16; + + if(filesize && (filesize != headersize)) { + fprintf(stderr, + SELF ": warning: %s file size (%d bytes) doesn't agree with " + "ATR header (%d bytes). File may be truncated or corrupt.\n", + infile, + filesize, + headersize); + } + + if(secsize == 128) { + fprintf(stderr, SELF ": single density image (128 bytes/sector)\n"); + seccount = filesize / secsize; + } else { + fprintf(stderr, SELF ": double density image (256 bytes/sector)\n"); + seccount = (filesize - 128 * 3) / secsize + 3; + } + + newseccount = seccount; + if(newsize == NULL) { + /* figure out appropriate new size for this image */ + if(secsize == 128) { + if(seccount < 720) + newseccount = 720; + else if(seccount > 720 && seccount < 1040) + newseccount = 1040; + else if(seccount >= 1040) { + fprintf(stderr, SELF ": must specify a sector count " + "for SD images >= 1040 sectors.\n"); + exit(1); + } + } else { + if(seccount < 720) + newseccount = 720; + else if(seccount > 720 && seccount < 1440) + newseccount = 1440; + else if(seccount >= 1440) { + fprintf(stderr, SELF ": must specify a sector count " + "for DD images >= 1440 sectors.\n"); + exit(1); + } + } + + /* + if(in) { + if(newseccount == seccount) { + fprintf(stderr, + SELF ": image is already %d sectors, nothing to do\n", seccount); + exit(0); + } else if(!newseccount) { + fprintf(stderr, + SELF ": image is already standard size, nothing to do\n"); + exit(0); + } + } + */ + } else { + newseccount = atoi(newsize); + if(newseccount < 3 || newseccount > 65535) { + fprintf(stderr, + SELF ": invalid sector count (must be 3 - 65535)\n"); + exit(1); + } + } + + if(newseccount < 368) { + fprintf(stderr, SELF ": warning: " + "Output image will not have Atari/MyDOS directory sectors.\n"); + } + + /* fix up ATR header */ + if(secsize == 128) + headersize = secsize * newseccount; + else + headersize = (128 * 3) + (newseccount - 3) * secsize; + + paras = headersize / 16; + buf[2] = paras & 0xff; + buf[3] = (paras >> 8) & 0xff; + buf[6] = (paras >> 16) & 0xff; + + if(in) { + /* make backup file */ + len = strlen(infile); + infile[len] = '~'; + infile[len + 1] = '\0'; + + unlink(infile); + if(link(outfile, infile)) { + fprintf(stderr, SELF ": can't create %s: %s\n", + infile, strerror(errno)); + exit(1); + } + + if(unlink(outfile)) { + fprintf(stderr, SELF ": can't delete %s: %s\n", + outfile, strerror(errno)); + exit(1); + } + } + + if( !(out = fopen(outfile, "wb")) ) { + fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno)); + exit(1); + } + + /* write ATR header */ + if(fwrite(buf, 1, 16, out) != 16) { + fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno)); + exit(1); + } + + /* read first 3 sectors (always present, always SD) */ + if(in) { + if(fread(buf, 1, 384, in) != 384) { + fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno)); + exit(1); + } + } else { + memset(buf, 0, 384); + } + + /* write first 3 sectors */ + if(fwrite(buf, 1, 384, out) != 384) { + fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno)); + exit(1); + } + + for(sec = 4; (sec <= seccount) && (sec <= newseccount); sec++) { + int i, has_data = 0; + + if(in) { + if(fread(buf, 1, secsize, in) != secsize) { + fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno)); + exit(1); + } + } + + for(i=0; i<secsize; i++) + has_data |= buf[i]; + + if(has_data) + last_data_sec = sec; + + if(fwrite(buf, 1, secsize, out) != secsize) { + fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno)); + exit(1); + } + } + + if(last_data_sec) + fprintf(stderr, SELF ": last non-empty sector was %d\n", last_data_sec); + else + fprintf(stderr, SELF ": image is blank (no data)\n"); + + if(newseccount < seccount) { + fprintf(stderr, SELF ": %s truncated to %d sectors, OK\n", + outfile, newseccount); + } else if(newseccount == seccount) { + fprintf(stderr, SELF ": %s rewritten at %d sectors, OK\n", + outfile, newseccount); + } else { + memset(buf, 0, secsize); + for( ; sec <= newseccount; sec++) { + if(fwrite(buf, 1, secsize, out) != secsize) { + fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno)); + exit(1); + } + } + fprintf(stderr, SELF ": %s extended to %d sectors, OK\n", + outfile, newseccount); + } + + if(in) fclose(in); + fclose(out); + exit(0); +} diff --git a/atrsize.rst b/atrsize.rst new file mode 100644 index 0000000..2c81f90 --- /dev/null +++ b/atrsize.rst @@ -0,0 +1,92 @@ +.. RST source for atrsize(1) man page. Convert with: +.. rst2man.py atrsize.rst > atrsize.1 +.. rst2man.py comes from the SBo development/docutils package. + +======= +atrsize +======= + +----------------------------------------------------------------------------- +Change the size of an Atari 8-bit ATR disk image, or create a blank ATR image +----------------------------------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +atrsize [**-bB**] *infile.atr* [*sectors*] + +DESCRIPTION +=========== + +Without the **-b** or **-B** options: + + *infile.atr* will be backed up to *infile.atr~*, and a new *infile.atr* will + be created. If *sectors* is given, the new image file will be truncated + or extended to the new size. Without *sectors*, the new image's size will + be set as follows: + + .. csv-table:: + :header: "Density","Original Sectors","New Sectors" + + "Either","0 - 2","Error" + "Either","3 - 719","720" + "Either","720","720 (no change)" + "Single","721 - 1039","1040 (aka 1050 enhanced density)" + "Single","1040","1040 (no change)" + "Single","1041 or more","Unknown (must specify size)" + "Double","721 - 1339","1440 (aka double sided, double density)" + "Double","1440","1440 (no change)" + "Double","1441 or more","Unknown (must specify size)" + + When *sectors* is given, its allowed range is from 3 to 65535. *infile.atr* + will be rewritten at the new size. + + When **atrsize** changes the size of an image, the new ATR header + will reflect the new size. If the new image is larger than the old + image, **atrsize** pads the image with empty sectors containing all 0 + data bytes. If the new image is smaller than the old image, it is + truncated, and any data in the old image that resides in the removed + sectors will be lost. + + For ATR images where the ATR header doesn't agree with the actual size + of the file, the actual file size is used to determine the number of + sectors. The output image will have its ATR header adjusted to reflect + the actual file size of the image, if sectors is not given. + +With **-b** or **-B**: + + **atrsize** will create a new, blank image called *infile.atr*. If + this file already exists, however, it will not be overwritten + (instead, **atrsize** will exit with a "file exists" message). + + If *sectors* is given, the new image's size will be set to that many + sectors. If not given, the new image's size will be 720 sectors. + + **-b** creates a new image with 128-byte sectors (single density) + + **-B** creates a new image with 256-byte sectors (double density). + + New images created with **atrsize** consist of a valid ATR header, + and sectors filled with zeroes. No boot sectors, directory, VTOC, + or filesystem are created. To use a blank image for file + storage, it must be formatted with an Atari DOS (either in an + emulator or with a real Atari via SIO2PC cable). If you're + trying to create a blank DOS 2.0S disk, use **axe -b**. + +NOTES +===== + +- **atrsize** cannot change the sector size (density) of an image under + any circumstances. Only the sector count may be changed. + +- **atrsize** will fail if the input file doesn't have a valid ATR header. + If it's an XFD (raw) image, use **xfd2atr**\(1) to convert it to an ATR first. + +EXIT STATUS +=========== + +Exit status is zero for success, non-zero for failure. + +.. include:: manftr.rst @@ -0,0 +1,166 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "AXE" 1 "2022-08-28" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +axe \- ATR/XFD Editor +.\" RST source for axe(1) man page. Convert with: +. +.\" rst2man.py axe.rst > axe.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fBaxe\fP [\fI\-alvtu\fP] [\fI\-b newimage\fP] [\fI\-D file\fP] [\fI\-x file\fP] [\fI\-w file\fP] [\fI\-c dirname\fP] [\fI\-t\fP] [\fI\-d sector\fP] [\fIimagefile\fP] +.SH DESCRIPTION +.sp +\fBaxe\fP allows the user to access files stored inside a single\-density +Atari DOS 2.0S disk image (ATR or XFD). It can list the directory, +copy files into and out of the image, delete files in the image, +create a new image (either blank or containing files), and dump various +low\-level information about the image\(aqs filesystem structure. +.SH OPTIONS +.SS Standard Options: +.INDENT 0.0 +.TP +.B \-b \fIfilename\fP +Create blank ATR image file called \fIfilename\fP\&. If \fIfilename\fP +already exists, it will be overwritten with no warning. +.TP +.B \-c \fIdirectory\fP +Create new ATR \fIimagefile\fP with contents of \fIdirectory\fP\&. If \fIimagefile\fP +already exists, it will be overwritten with no warning. Similar to \fBtar cf\fP\&. +.TP +.B \-D \fIfile\fP +Delete \fIfile\fP from \fIimagefile\fP\&. Ignores "locked" bit. +.TP +.B \-t \fIdirectory\fP +Extract all files in image to \fIdirectory\fP, which will be created and +must not already exist. Similar to \fBtar xf\fP\&. +.UNINDENT +.INDENT 0.0 +.TP +.B \-u +Unix <\-> Atari newline/EOL translation (use for text files only; breaks other file types). +.UNINDENT +.INDENT 0.0 +.TP +.B \-w \fIfile\fP +Write \fIfile\fP to \fIimagefile\fP, overwrites if \fIfile\fP already exists. Ignores "locked" bit. +.TP +.B \-x \fIfile\fP +Extract (read) file from \fIimagefile\fP, write to current directory. +.UNINDENT +.SS Debugging Options: +.INDENT 0.0 +.TP +.B \-a +List all directory entries, even deleted/empty ones. +.UNINDENT +.INDENT 0.0 +.TP +.B \-d \fIsector\fP +Dump a sector in decimal, hex, and binary. +.UNINDENT +.INDENT 0.0 +.TP +.B \-l +Trace and print sector links for all files on disk. +.TP +.B \-v +Dump VTOC (sector 360) in decimal, hex, and binary. +.UNINDENT +.SH LIMITATIONS +.sp +\fBaxe\fP is ancient code, from last century. It has various design +flaws and bugs. At this point, it would be better to rewrite it from +scratch than to try & fix the existing code. +.sp +Only Atari DOS 2.0S and 100% compatible single\-density disk images are +supported. MyDOS images will work, but there\(aqs no support for MyDOS +subdirectories. There\(aqs no support for e.g. SpartaDOS or Atari DOS +3.0/4.0. Atari DOS 2.5 enhanced density images will work, the same +way they work on DOS 2.0S: files using the extra sectors will not be +readable, and \fBaxe\fP won\(aqt write to the extra sectors. Atari DOS 1.0 +images (which are \fIvery\fP rare) can at least have the directory listed, +but I wouldn\(aqt recommend writing to them. +.sp +The "file locked" (aka read\-only) bit is ignored when writing, and +there\(aqs no way to lock or unlock files, though locked files do appear +with * next to the name in the directory listing. +.sp +It\(aqs possible to create files in a disk image with invalid filenames, +e.g. beginning with a number, or containing punctuation. Atari DOS +might or might not be able to even delete such files, but \fBaxe\fP will +be able to if it happens. +.sp +One known bug: when writing a file to an image, if the file\(aqs size +is a multiple of 125 (the number of data bytes per sector), an extra +sector is allocated (with 0 data bytes in it). This doesn\(aqt actually +cause a problem for Atari DOSes (it just wastes space on the disk), +but it prevents simple\-minded XEX loaders like Fenders from being able +to load the file (technically this is a bug in Fenders, too). Atari +DOSes actually can create files like this if they\(aqre opened for +append, then closed without writing new data. +.sp +\fBaxe\fP does nothing with the boot sectors (sectors 1\-3) of the disk +image. When creating a new image, the boot sectors will be blank (all +zeroes), meaning the disk won\(aqt be bootable. If DOS.SYS is written to +an image with a DOS boot record, the boot record won\(aqt be updated with +the first sector of DOS.SYS, so the disk won\(aqt be bootable. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. @@ -0,0 +1,197 @@ +/* axe - manipulate atari disk images (dos 2.0s only for now) + * + * Usage: ataridir [-a] [-x filename] [-l] [-v] <disk_image.atr> + * + * (-a means list all directory entries, regardless of status byte) + * -l means dump sector link info for all filenumbers on disk + * -x means extract file from image + * -v means hex dump of VTOC sector + * + */ + +/* indented with: indent -kr -nsai -nsaw -ts3 -i3 -br -brf -brs -cdw -ce */ + +#include "axe.h" + +int all = 0, total_sec, translate = 0; + + +int main(int argc, char *argv[]) { +#ifdef DEBUG + int vtoc_dump = 0; +#endif + int extract = 0, del = 0, dump_sec = 0, print = 1, + opt, write = 0, filenum = 0, i, j, + lng = 0, tar = 0, blank = 0, create = 0; + unsigned char buf[256], diskbuf[720 * 128 + 16]; + char newblank[13]; + unsigned char *debuf; + char filename[13] = { "\0" }; + char fnbuf[13] = " \0"; + DIR *credir; + struct dirent *nfile; + + printf("axe (the ATR/XFD Editor) v%s, (c) B. Watson.\n", VERSION); + printf("Released under the WTFPL.\n\n"); + if(argc < 2) + usage(argv[0]); + while((opt = getopt(argc, argv, "ab:c:lvd:D:x:tuw:")) != EOF) { + switch (opt) { + case 'a': + all = 1; + break; + case 'b': + /*write_blank_disk(optarg); */ + blank = 1; + strcpy(newblank, optarg); + break; + case 'c': + print = 0; + create = 1; + strcpy(filename, optarg); + break; + case 'd': + dump_sec = atoi(optarg); + break; + case 'l': + lng = 1; + break; + case 'v': + dump_sec = 360; + break; + case 'D': + del = 1; + print = 0; + strcpy(filename, optarg); + break; + case 'x': + strcpy(filename, optarg); + extract = 1; + print = 0; + break; + case 'w': + write = 1; + print = 0; + strcpy(filename, optarg); + break; + case 't': + tar = 1; + break; + case 'u': + translate = 1; + break; + case '?': + case ':': + exit(1); + default: + printf("How'd I get here?\n"); + exit(255); + } + } + + if((create + write + extract + del + tar) > 1) { + printf("Only one of -t, -w, -c, -x, -D may be specified.\n"); + exit(1); + } + if(blank) + write_blank_disk(newblank); + if(optind == argc) { + if(!blank) + usage(argv[0]); + exit(!blank); + } + printf("Using image file %s\n\n", argv[argc - 1]); +#ifdef DEBUG + printf("-a:%d -l:%d\n -v:%d", all, lng, vtoc_dump); +#endif + if(filename[0] && (!create)) { + parse_filename(filename, fnbuf); + } +#ifdef DEBUG + printf("Passed filename %s, parsed as %s\n", filename, fnbuf); +#endif + if(create) + write_blank_disk(argv[argc - 1]); + load_disk(diskbuf, argv[argc - 1]); +/* if(write) { + if(strcmp(filename,argv[argc-1])==0) { + printf("No disk image name given - aborting\n"); + exit(1); + } + write_file(diskbuf,filename); + write_disk(diskbuf,argv[argc-1]); + exit(0); + } */ +#ifdef DEBUG + printf("-t=%d\n", tar); +#endif + if(create) { + char msgbuf[256]; + if((credir = opendir(filename)) == NULL) { + sprintf(msgbuf, "can't open directory %s", filename); + perror(msgbuf); + exit(1); + } + while((nfile = readdir(credir)) != NULL) { + if(nfile->d_name[0] == '.') + continue; + write_file(diskbuf, nfile->d_name); + } + printf("Done.\n"); + write_disk(diskbuf, argv[argc - 1]); + exit(0); + } + + + if(tar) { + make_dir(argv[argc - 1]); + } + + for (i = 361; i < 369; i++) { + read_sector(diskbuf, i, buf); + for (j = 0; j < 128; j += 16) { + debuf = buf + j; + if((tar) && (debuf[0] & 64) && !(debuf[0] & 128)) { + strncpy(fnbuf, (char *)(debuf + 5), 11); + make_filename(fnbuf, filename); + dump_file(diskbuf, debuf, filename); + } + if(print && !(debuf[0] & 128)) + print_entry(debuf); +#ifdef DEBUG + printf("strncmp: %s, %s\n", fnbuf, debuf + 5); +#endif + if((strncmp(fnbuf, (char *)(debuf + 5), 11) == 0) && !(debuf[0] & 128)) { +#ifdef DEBUG + printf("matched filename\n"); +#endif + if(extract) + dump_file(diskbuf, debuf, filename); + if(del || write) { + traverse_file(diskbuf, debuf, TF_DELETE); + debuf[0] |= 128; + write_sector(diskbuf, i, buf); + write_disk(diskbuf, argv[argc - 1]); + } + } + + if(lng) + traverse_file(diskbuf, debuf, TF_PRINT); + + filenum++; + } + } + if(print) { + read_sector(diskbuf, 360, buf); + i = buf[3] + 256 * buf[4]; + printf("%03d FREE SECTORS\n", i); +/* if(i!=(707-total_sec)) printf("Warning: VTOC free sectors != (707 - total_sec), %d vs. %d\n",i,707-total_sec);*/ + } + if(dump_sec) + dump_sector(diskbuf, dump_sec); + if(write) { + write_file(diskbuf, filename); + write_disk(diskbuf, argv[argc - 1]); + } + exit(0); +} @@ -0,0 +1,52 @@ +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <ctype.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <fcntl.h> +#include <unistd.h> +#include <getopt.h> +#include <dirent.h> + +extern char *optarg; +extern int optind, optopt, opterr; + +/* struct atari_dirent { + unsigned char flag; // bit 7=deleted 6=normal 5=locked 0=open4write + unsigned char countlo; + unsigned char counthi; + unsigned char startlo; + unsigned char starthi; + char namelo[8]; + char namehi[3]; +}; +*/ + +#define TF_PRINT 1 +#define TF_DELETE 2 + +int load_disk(unsigned char *disk, char *); +int write_disk(unsigned char *disk, char *); + +int get_confirm(char *); +void usage(char *); +void print_entry(unsigned char *buf); +/*void dump_links(unsigned char *disk,unsigned char *ent);*/ +void traverse_file(unsigned char *disk, unsigned char *ent, int ACTION); +void dump_file(unsigned char *disk, unsigned char *ent, char *); +int parse_filename(char *filename, char *result); +int make_filename(char *filename, char *result); +int make_dir(char *filename); +void dump_sector(unsigned char *disk, int); +void read_sector(unsigned char *disk, int, unsigned char *buf); +void write_sector(unsigned char *disk, int, unsigned char *buf); +void write_blank_disk(char *filename); +void write_file(unsigned char *disk, char *filename); +/*int check_dir(unsigned char *disk,char *filename);*/ +int get_dentry(unsigned char *disk); +int get_free_sector(unsigned char *disk); +void mark_used(unsigned char *vtoc_sector, int sector); +void mark_free(unsigned char *vtoc_sector, int sector); +int vtoc_byte(int sector); +int vtoc_bit(int sector); @@ -0,0 +1,114 @@ +.. RST source for axe(1) man page. Convert with: +.. rst2man.py axe.rst > axe.1 +.. rst2man.py comes from the SBo development/docutils package. + +=== +axe +=== + +-------------- +ATR/XFD Editor +-------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +**axe** [*-alvtu*] [*-b newimage*] [*-D file*] [*-x file*] [*-w file*] [*-c dirname*] [*-t*] [*-d sector*] [*imagefile*] + +DESCRIPTION +=========== + +**axe** allows the user to access files stored inside a single-density +Atari DOS 2.0S disk image (ATR or XFD). It can list the directory, +copy files into and out of the image, delete files in the image, +create a new image (either blank or containing files), and dump various +low-level information about the image's filesystem structure. + +OPTIONS +======= + +Standard Options: +----------------- + +-b *filename* + Create blank ATR image file called *filename*. If *filename* + already exists, it will be overwritten with no warning. + +-c *directory* + Create new ATR *imagefile* with contents of *directory*. If *imagefile* + already exists, it will be overwritten with no warning. Similar to **tar cf**. + +-D *file* + Delete *file* from *imagefile*. Ignores "locked" bit. + +-t *directory* + Extract all files in image to *directory*, which will be created and + must not already exist. Similar to **tar xf**. + +-u + Unix <-> Atari newline/EOL translation (use for text files only; breaks other file types). + +-w *file* + Write *file* to *imagefile*, overwrites if *file* already exists. Ignores "locked" bit. + +-x *file* + Extract (read) file from *imagefile*, write to current directory. + +Debugging Options: +------------------ + +-a + List all directory entries, even deleted/empty ones. + +-d *sector* + Dump a sector in decimal, hex, and binary. + +-l + Trace and print sector links for all files on disk. + +-v + Dump VTOC (sector 360) in decimal, hex, and binary. + +LIMITATIONS +=========== + +**axe** is ancient code, from last century. It has various design +flaws and bugs. At this point, it would be better to rewrite it from +scratch than to try & fix the existing code. + +Only Atari DOS 2.0S and 100% compatible single-density disk images are +supported. MyDOS images will work, but there's no support for MyDOS +subdirectories. There's no support for e.g. SpartaDOS or Atari DOS +3.0/4.0. Atari DOS 2.5 enhanced density images will work, the same +way they work on DOS 2.0S: files using the extra sectors will not be +readable, and **axe** won't write to the extra sectors. Atari DOS 1.0 +images (which are *very* rare) can at least have the directory listed, +but I wouldn't recommend writing to them. + +The "file locked" (aka read-only) bit is ignored when writing, and +there's no way to lock or unlock files, though locked files do appear +with \* next to the name in the directory listing. + +It's possible to create files in a disk image with invalid filenames, +e.g. beginning with a number, or containing punctuation. Atari DOS +might or might not be able to even delete such files, but **axe** will +be able to if it happens. + +One known bug: when writing a file to an image, if the file's size +is a multiple of 125 (the number of data bytes per sector), an extra +sector is allocated (with 0 data bytes in it). This doesn't actually +cause a problem for Atari DOSes (it just wastes space on the disk), +but it prevents simple-minded XEX loaders like Fenders from being able +to load the file (technically this is a bug in Fenders, too). Atari +DOSes actually can create files like this if they're opened for +append, then closed without writing new data. + +**axe** does nothing with the boot sectors (sectors 1-3) of the disk +image. When creating a new image, the boot sectors will be blank (all +zeroes), meaning the disk won't be bootable. If DOS.SYS is written to +an image with a DOS boot record, the boot record won't be updated with +the first sector of DOS.SYS, so the disk won't be bootable. + +.. include:: manftr.rst diff --git a/axelib.c b/axelib.c new file mode 100644 index 0000000..2b6e66b --- /dev/null +++ b/axelib.c @@ -0,0 +1,546 @@ +#include "axe.h" + +extern int all, total_sec, translate; + +static int atr_offset; + +int load_disk(unsigned char *diskbuf, char *filename) { + FILE *file; + int vtoc_sec; + unsigned char tmpbuf[16]; + char tmp[128]; + + if((file = fopen(filename, "r")) == NULL) { + sprintf(tmp, "load_disk(): %s", filename); + perror(tmp); + exit(1); + } + fread(tmpbuf, 16, 1, file); + atr_offset = 16; + memcpy(diskbuf, tmpbuf, 16); + if((tmpbuf[0] != 0x96) || (tmpbuf[1] != 0x02)) { /* not an atr */ + atr_offset = 0; + rewind(file); + total_sec = 720; + } + + /* do some sanity checking of images: */ + if(atr_offset) { + total_sec = (diskbuf[2] + 256 * diskbuf[3]) >> 3; + if(!total_sec) + total_sec = (diskbuf[6] + 256 * diskbuf[7]) * 8192; + if(total_sec < 720) { + printf("Total sectors (%d) < 720 - aborting\n", total_sec); + exit(1); + } + if(total_sec > 720) { + printf + ("Warning: Total sectors (%d) > 720 - only reading first 720 sectors\n", + total_sec); + } + if((diskbuf[4] + 256 * diskbuf[5]) != 128) { + printf("Fatal: sector size not 128 bytes!\n"); + exit(1); + } + } else { + printf + ("Warning: blindly assuming xfd image is 720 sectors, 128 bytes/sector!\n"); + } + + if((total_sec = fread(diskbuf + atr_offset, 128, 720, file)) < 720) { + printf("Error reading image file, only got %d sectors, aborting\n", total_sec); + perror("fread"); + exit(1); + } + + vtoc_sec = + diskbuf[atr_offset + 128 * 359 + 1] + 256 * diskbuf[atr_offset + + 128 * 359 + 2]; + if(vtoc_sec != 707) + printf + ("Warning: VTOC sector count (%d) not 707, are you sure this is an AtariDOS 2.0 image?\n", + vtoc_sec); + return 0; +} + +int write_disk(unsigned char *diskbuf, char *filename) { + FILE *file; + unsigned char tmpbuf[16]; + + file = fopen(filename, "w"); + memcpy(tmpbuf, diskbuf, 16); + atr_offset = 0; + if((tmpbuf[0] == 0x96) && (tmpbuf[1] == 0x02)) { /* is an atr */ + fwrite(tmpbuf, 16, 1, file); + atr_offset = 16; + } + fwrite(diskbuf + atr_offset, 128, 720, file); + fclose(file); + return 0; +} + + +void usage(char *name) { + printf + ("Usage: %s [options] [-D filename] [-w filename] [-e filename]\n\t\t [-b filename] [-c dirname] [-d sector] disk_image\n\n", + name); + + printf("\t-a\tlist all dir entries, even deleted/empty ones\n"); + printf("\t-l\ttrace & print links for all files on disk\n"); + printf("\t-v\tdump VTOC (sector 360) in decimal, hex, and binary\n"); + printf + ("\t-t\t'tar xf' style - create dir filled with files in image\n"); + printf("\t-u\tUn*x <-> Atari newline/EOL translation\n"); + printf("\t-D\tdelete file from image\n"); + printf("\t-w\twrite file to image\n"); + printf("\t-c\tcreate new image from directory\n"); + printf("\t-x\textract (read) file from image, write to current dir\n"); + printf("\t-b\tcreate blank image called 'filename'\n"); + printf("\t-d\tdump a sector in decimal, hex, and binary\n"); + printf + ("\ndisk_image must be a 720 sector, single density .atr or .xfd image\n"); + printf("\tin Atari DOS 2.0 or compatible format\n"); + printf + ("\nAll UN*X filenames must conform to Atari 8.3 filename.ext format\n"); + printf("\t(though they are NOT treated case-sensitively)\n"); + printf("\nEnjoy!\n\n"); + exit(1); +} + +void print_entry(unsigned char *buf) { +#ifdef DEBUG + static int ents; + int i; +#endif + int s; + unsigned char tmpbuf[32]; + + memcpy(tmpbuf, buf, 16); + tmpbuf[16] = '\0'; +#ifdef DEBUG + printf("successfully read dir. entry #%d\n", ents++); +#endif + if((tmpbuf[0] & 64) || all) { +#ifdef DEBUG + for (i = 0; i < 16; i++) { + printf(" 0x%X", tmpbuf[i]); + } +#endif + total_sec += (s = tmpbuf[1] + 256 * tmpbuf[2]); + printf("%c %s\t%03d\n", (tmpbuf[0] & 32) ? '*' : ' ', tmpbuf + 5, + s); + } +} + +void read_sector(unsigned char *disk, int sector, unsigned char *buf) { + memcpy(buf, disk + (sector - 1) * 128 + atr_offset, 128); +} + +void write_sector(unsigned char *disk, int sector, unsigned char *buf) { + memcpy(disk + (sector - 1) * 128 + atr_offset, buf, 128); +} + +void traverse_file(unsigned char *disk, unsigned char *ent, int action) { + int i, next_sec, data_bytes, curfile; + unsigned char buf[128]; + unsigned char vtoc[128]; + +#ifdef DEBUG + printf("traverse_file called with %s\n", + (action - 1) ? "TF_DELETE" : "TF_PRINT"); +#endif + + if(action == TF_DELETE) + read_sector(disk, 360, vtoc); + + next_sec = ent[3] + 256 * ent[4]; + while(next_sec) { + if(action == TF_DELETE) + mark_free(vtoc, next_sec); + if(action == TF_PRINT) + printf("%d: ", next_sec); + read_sector(disk, next_sec, buf); + data_bytes = buf[127]; + curfile = buf[125] >> 2; + next_sec = buf[126] + 256 * (buf[125] & 3); + if(action == TF_PRINT) + printf("data_bytes=%d filenum=%d next_sec=%d\n", data_bytes, + curfile, next_sec); + } + if(action == TF_DELETE) { + i = (vtoc[3] + 256 * vtoc[4]); +#ifdef DEBUG + printf("deleting file, old vtoc free count=%d", i); +#endif + i += ent[1] + 256 * ent[2]; +#ifdef DEBUG + printf(", new=%d\n", i); +#endif + vtoc[3] = i % 256; + vtoc[4] = i / 256; + write_sector(disk, 360, vtoc); + if(action == TF_DELETE) { + printf("Deleted file, now %d free sectors on image\n", i); + } + } +} + +void dump_file(unsigned char *disk, unsigned char *ent, char *outfile) { +#ifdef DEBUG + int curfile; +#endif + int i, next_sec, data_bytes, total_written = 0; + unsigned char buf[128]; + FILE *out; + +/* fseek(handle,atr_seek(361)+16*filenum,SEEK_SET); + fread(buf,16,1,handle); */ + next_sec = ent[3] + 256 * ent[4]; + out = fopen(outfile, "wb"); +/* #ifdef DEBUG + printf("First sector of filenum %d, ",filenum); + #endif */ + while(next_sec) { +#ifdef DEBUG + printf("%d: ", next_sec); +#endif + read_sector(disk, next_sec, buf); + data_bytes = buf[127]; + next_sec = buf[126] + 256 * (buf[125] & 3); +#ifdef DEBUG + curfile = buf[125] >> 2; + printf("data_bytes=%d filenum=%d next_sec=%d\n", data_bytes, curfile, + next_sec); +#endif + if(translate) + for (i = 0; i <= data_bytes; i++) + if(buf[i] == 0x9b) + buf[i] = '\n'; + fwrite(buf, data_bytes, 1, out); + total_written += data_bytes; + fflush(out); + } + printf("Wrote %d bytes to file %s\n", total_written, outfile); +} + + +int parse_filename(char *filename, char *result) { + int i = 0, j = 0; + char ext[4] = " \0"; + char fn[9] = " \0"; +#ifdef DEBUG + printf("parse_filename called, filename=%s\n", filename); +#endif + if(strlen(filename) > 12) { + printf("filename too long: %s\n", filename); + exit(1); + } +/* if(strlen(strstr(filename,"."))>4) { + printf("extension too long: %s\n",filename); + exit(1); + } */ + while((filename[i]) && (filename[i] != '.')) { + fn[i++] = toupper(filename[i]); + } + if(filename[i] == '.') + while(filename[++i]) { + ext[j++] = toupper(filename[i]); + } + + strcpy(result, fn); + strncat(result, ext, 3); +#ifdef DEBUG + printf("parse_filename exiting, result=%s\n", result); +#endif + return (0); +} + +int make_filename(char *filename, char *result) { + int i = 0, j = 8; + char fn[13] = { '\0' }; + + while((filename[i] && filename[i] != ' ') && (i < 8)) { + fn[i++] = tolower(filename[i]); + } + if(filename[j] != ' ') { + fn[i++] = '.'; + while(filename[j]) { + fn[i++] = tolower(filename[j++]); + } + } +#ifdef DEBUG + printf("made filename %s from %s\n", fn, filename); +#endif + strcpy(result, fn); + return 0; +} + +int make_dir(char *filename) { + char tmp[15] = { '\0' }; + char *p; + + strcpy(tmp, filename); + if(!(p = strrchr(tmp, '.'))) { + strcat(tmp, ".dir"); + } else { + *p = '\0'; + } +#ifdef DEBUG + printf("made dir: %s\n", tmp); +#endif + if(mkdir(tmp, 0777)) { + perror("Can't make directory: "); + exit(1); + } + chdir(tmp); + return 0; +} + +void dump_sector(unsigned char *disk, int sector) { + unsigned char buf[256]; + int i, j, k; + printf("Dump of sector %d in decimal:\n", sector); + read_sector(disk, sector, buf); + for (i = 0; i < 128; i += 16) { + printf("%03d: ", i); + for (j = 0; j < 16; j++) { + printf("%03d ", buf[i + j]); + } + printf("\n"); + } + printf("Dump of sector %d in hex:\n", sector); + for (i = 0; i < 128; i += 16) { + printf("%03x: ", i); + for (j = 0; j < 16; j++) { + printf(" %02x ", buf[i + j]); + } + printf("\n"); + } + printf("Dump of sector %d in binary:\n", sector); + for (i = 0; i < 128; i += 16) { + printf("%03x: ", i); + for (j = 0; j < 16; j++) { + for (k = 7; k > -1; k--) { + if(buf[i + j] & (1 << k)) + printf("1"); + else + printf("0"); + } + printf(" "); + if(j == 7) + printf("\n "); + } + printf("\n"); + } +} + + +/* please, ignore the ugliness of this function:*/ +void write_blank_disk(char *filename) { + unsigned char atr_header[16] = { 0x96, 0x02, 0x80, 0x16, 0x80, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + unsigned char disk[128 * 720 + 16] = { '\0' }; + int i; + unsigned char vtoc[128] = + { 0x02, 0xc3, 0x02, 0xc3, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f + }; + + for (i = 11; i < 100; i++) + vtoc[i] = 0xff; + vtoc[55] = 0x00; + vtoc[56] = 0x7f; + for (i = 100; i < 128; i++) + vtoc[i] = 0x00; + atr_offset = 16; + memcpy(disk, atr_header, 16); + write_sector(disk, 360, vtoc); + write_disk(disk, filename); + printf("Wrote formatted blank image to %s\n", filename); +} + +static void xlate_eofs(unsigned char *buf, int len) { + int i; + for (i = 0; i < len; i++) + if(buf[i] == '\n') + buf[i] = 0x9b; +} + +void write_file(unsigned char *disk, char *filename) { + unsigned char vtoc[128]; + unsigned char buf[128]; + char fnbuf[15]; +/* unsigned char dentry[16];*/ + unsigned char *dindex; + int i, dentryno, first_sec, secno, next_sec, count = 0, bytes; + FILE *handle; + + printf("Writing %s to image...", filename); + parse_filename(filename, fnbuf); + if((handle = fopen(filename, "r")) == NULL) { + perror("write_file::fopen"); + exit(1); + } +#ifdef DEBUG + printf("write_file: parsed %s as \"%s\"\n", filename, fnbuf); +#endif + if((dentryno = get_dentry(disk)) == -1) { + printf("write_file: no free directory entries on image\n"); + exit(1); + } +#ifdef DEBUG + printf("dentryno=%d\n", dentryno); +#endif + read_sector(disk, 360, vtoc); + if(!(first_sec = secno = get_free_sector(vtoc))) { + printf("write_file: no free sectors in image\n"); + exit(1); + } +#ifdef DEBUG + printf("first_sec=%d\n", first_sec); +#endif + do { + int tmp, eof = 0; + count++; + bytes = fread(buf, 1, 125, handle); + + if(bytes == 125) { + /* test for EOF on handle */ + tmp = fgetc(handle); + if(tmp == EOF) + eof = 1; + else + ungetc(tmp, handle); + } + + if(translate) xlate_eofs(buf, 125); + mark_used(vtoc, secno); + + if(bytes == 125 && !eof) { + /* full sector, not at EOF */ + next_sec = get_free_sector(vtoc); + buf[125] = (dentryno << 2) | (next_sec >> 8); + buf[126] = next_sec & 255; + buf[127] = 125; + write_sector(disk, secno, buf); + secno = next_sec; + } else { + /* partial sector or full sector at EOF */ + buf[125] = dentryno << 2; + buf[126] = 0; + buf[127] = bytes; + write_sector(disk, secno, buf); + next_sec = 0; + } + } while(next_sec); + /* update dentry */ + read_sector(disk, 361 + (dentryno / 8), buf); + dindex = buf + (dentryno % 8) * 16; + dindex[0] = 66; /* taken from a dos 2.0s disk, hope its right */ + dindex[1] = count & 255; + dindex[2] = count / 256; + dindex[3] = first_sec & 255; + dindex[4] = first_sec / 256; + memcpy(dindex + 5, fnbuf, 11); + write_sector(disk, 361 + (dentryno / 8), buf); + /*all thats left to do is update free sector count in vtoc: */ + i = (vtoc[3] + 256 * vtoc[4]) - count; + vtoc[3] = i & 255; + vtoc[4] = i / 256; + write_sector(disk, 360, vtoc); + printf("Wrote %d sectors.\n", count); +} + +int get_dentry(unsigned char *disk) { + int i = 361, j, dentryno = 0, done = 0; + unsigned char buf[128]; + + while(!done) { + read_sector(disk, i, buf); + for (j = 0; j < 128; j += 16) { + if((buf[j] == 0) || (buf[j] & 128)) { + return dentryno; + } + dentryno++; + } + done = (++i == 369); + } + return -1; +} + +int get_free_sector(unsigned char *vtoc) { + int i; + + for (i = 1; i < 720; i++) { /* atari DOS doesn't use sector 720 */ + if(vtoc[vtoc_byte(i)] & vtoc_bit(i)) + return i; + } + + return 0; +} + +int vtoc_byte(int sector) { + return 10 + (sector) / 8; +} + +int vtoc_bit(int sector) { + return 128 >> ((sector) % 8); +} + +void mark_used(unsigned char *vtoc_sector, int sector) { + vtoc_sector[vtoc_byte(sector)] &= ~vtoc_bit(sector); +} + +void mark_free(unsigned char *vtoc_sector, int sector) { + vtoc_sector[vtoc_byte(sector)] |= vtoc_bit(sector); +} + +/*int get_confirm(char *prompt) { + printf("%s[y/N]? ",prompt); + return ((toupper(getc(stdin)))=='Y'); +}*/ + +int check_dir(unsigned char *disk, char *filename) { + int i = 361, j, done = 0, dentryno = 0; + unsigned char buf[128]; + + printf("check_dir: passed %s\n", filename); + while(!done) { + read_sector(disk, i, buf); + for (j = 0; j < 128; j += 16) { + if((strncmp(filename, (char*)(buf + j + 5), 11) == 0)) { + return dentryno; + } + dentryno++; + } + done = (++i == 369); + } + + return -1; +} + +/* these routines suck, and are not actually used any more. + * maybe someday they'll become useful again. + * + int delete_file(unsigned char *disk,char *filename) { + int first_sec,next_sec,i,dentryno; + char fnbuf[15]; + char buf[128]; + char *debuf; + char vtoc[128]; + + printf("delete_file doesn't work yet. passed: %s\n",filename); + parse_filename(filename,fnbuf); + if((dentryno=check_dir(disk,fnbuf))==-1) { + printf("delete_file: file %s not found on image\n",fnbuf); + exit(1); + } + printf("delete_file: would delete %s\n",fnbuf); + read_sector(disk,361+dentryno/8,buf); + read_sector(disk,360,vtoc); + debuf=buf+(dentryno%8)*16; + next_sec=debuf[3]+256*debuf[4]; + traverse_file(disk,debuf,TF_DELETE); + return 0; +} +*/ diff --git a/blob2c.1 b/blob2c.1 new file mode 100644 index 0000000..6553931 --- /dev/null +++ b/blob2c.1 @@ -0,0 +1,137 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "BLOB2C" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +blob2c \- Create C source and header files from a binary file +.\" RST source for blob2c(1) man page. Convert with: +. +.\" rst2man.py blob2c.rst > blob2c.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +blob2c \fIblobfile\fP > \fIoutput.c\fP 2> \fIoutput.h\fP +.SH DESCRIPTION +.sp +\fBblob2c\fP prints a C source file to standard output, containing +\fIan unsigned char\fP array, initialized to the contents of \fIblobfile\fP, +and an \fIint\fP, initialized to the length of \fIblobfile\fP in bytes. +.sp +The name of the array is based on the input filename, with +non\-alphanumeric characters replaced by underscores. The name of the +int is the array name, plus the string \fB_len\fP\&. +.sp +Also, a header file containing a pair of extern declarations is +printed to standard error output. This header may be included multiple +times in the same translation unit, since it contains "guard" +preprocessor code to prevent multiple declarations. +.sp +\fBblob2c\fP takes no options, and requires exactly one argument (the +input filename \fIblobfile\fP). +.sp +Exit status is zero for success and non\-zero for failure. Error +messages are printed to standard error output as preprocessor +\fB#error\fP directives, since standard error is expected to be +redirected to a header file. The \fB#error\fP directives will cause the +error messages to be printed when the header file is compiled. +.sp +Although it\(aqs distributed with the author\(aqs Atari 8\-bit +utilities, there\(aqs nothing Atari\-specific about \fBblob2c\fP\&. It could be +useful for any situation where you need to include a file\(aqs contents +as an array in a C program. +.SH EXAMPLE +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +$ echo "Hello, World." > hello.bin + +$ blob2c hello.bin >hello.c 2>hello.h + +## check exit status (0=success) +$ echo $? +0 + +$ cat hello.h +/* C header created by blob2c from input file hello.bin */ + +#ifndef hello_bin_H +#define hello_bin_H + +extern unsigned char hello_bin[]; +extern int hello_bin_len; + +#endif /* hello_bin_H */ + +$ cat hello.c + +/* C source created by blob2c from input file hello.bin */ + +unsigned char hello_bin[] = { + /* 0 */ 0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57, /* Hello, W */ + /* 8 */ 0x6f,0x72,0x6c,0x64,0x2e,0x0a /* orld.. */ +}; /* hello_bin */ + +int hello_bin_len = 14; +.ft P +.fi +.UNINDENT +.UNINDENT +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/blob2c.c b/blob2c.c new file mode 100644 index 0000000..779b79f --- /dev/null +++ b/blob2c.c @@ -0,0 +1,117 @@ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <ctype.h> + +/* since we're emitting C source, the comments containing the + ASCII dump better not contain C comment markers! */ +void fixasc(char *asc, int len) { + int j; + + for(j=0; j<(len-1); j++) { + /* slash-star or slash-slash become slash-dot */ + if(asc[j] == '/' && (asc[j+1] == '*' || asc[j+1] == '/')) + asc[j+1] = '.'; + /* star-slash becomes dot-slash */ + else if(asc[j] == '*' && asc[j+1] == '/') + asc[j] = '.'; + } +} + +int main(int argc, char **argv) { + int i, j, count = 0; + char *p, asc[9]; + FILE *in; + + if(argc != 2) { + fprintf(stderr, + "#error usage: %s blobfile >blobfile.c 2>blobfile.h\n", argv[0]); + exit(1); + } + + if( !(in = fopen(argv[1], "rb")) ) { + fprintf(stderr, "/* %s: %s */\n", argv[1], strerror(errno)); + exit(1); + } + + printf("/* C source created by blob2c from input file %s */\n\n", argv[1]); + fprintf(stderr, + "/* C header created by blob2c from input file %s */\n\n", argv[1]); + + for(p = argv[1]; *p; p++) + if(!isalnum(*p)) *p = '_'; + + fprintf(stderr, + "#ifndef %s_H\n" + "#define %s_H\n\n", + argv[1], argv[1]); + + /* start array definition */ + printf("unsigned char %s[] = {", argv[1]); + + asc[8] = '\0'; + + /* read/process each input byte in loop, until EOF */ + while( (i = getc(in)) != EOF ) { + if(count) putchar(','); + + if(count % 8 == 0) { + if(count) { + /* fix & print ASCII dump */ + fixasc(asc, 8); + printf(" /* %s */", asc); + } + + /* start next line */ + printf("\n\t/* %6d */ ", count); + } + + /* store this byte of ASCII dump */ + asc[count % 8] = isprint(i) ? i : '.'; + + /* print this byte of hex data */ + printf("0x%02x", i); + + count++; + } + + /* fix ASCII dump for last line */ + i = count % 8; + if(!i) i = 8; + fixasc(asc, i); + asc[i] = '\0'; + + /* line it up with the previous lines */ + j = (8 - i) * 5; + for(i=0; i<j; i++) + putchar(' '); + + /* print it */ + printf(" /* %s */", asc); + + /* end of array definition */ + printf("\n}; /* %s */\n\n", argv[1]); + + /* array_len definition */ + printf("int %s_len = %d;\n", argv[1], count); + + /* check for read errors */ + i = 0; + if(ferror(in)) { + fprintf(stderr, "#error %s: %s\n", argv[1], strerror(errno)); + i = 1; + } + + fclose(in); + + /* extern declarations to stderr */ + fprintf(stderr, + "extern unsigned char %s[];\nextern int %s_len;\n", + argv[1], argv[1]); + + fprintf(stderr, "\n#endif /* %s_H */\n", argv[1]); + + return i; +} diff --git a/blob2c.rst b/blob2c.rst new file mode 100644 index 0000000..5db5cc2 --- /dev/null +++ b/blob2c.rst @@ -0,0 +1,85 @@ +.. RST source for blob2c(1) man page. Convert with: +.. rst2man.py blob2c.rst > blob2c.1 +.. rst2man.py comes from the SBo development/docutils package. + +====== +blob2c +====== + +--------------------------------------------------- +Create C source and header files from a binary file +--------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +blob2c *blobfile* > *output.c* 2> *output.h* + +DESCRIPTION +=========== + +**blob2c** prints a C source file to standard output, containing +*an unsigned char* array, initialized to the contents of *blobfile*, +and an *int*, initialized to the length of *blobfile* in bytes. + +The name of the array is based on the input filename, with +non-alphanumeric characters replaced by underscores. The name of the +int is the array name, plus the string **_len**. + +Also, a header file containing a pair of extern declarations is +printed to standard error output. This header may be included multiple +times in the same translation unit, since it contains "guard" +preprocessor code to prevent multiple declarations. + +**blob2c** takes no options, and requires exactly one argument (the +input filename *blobfile*). + +Exit status is zero for success and non-zero for failure. Error +messages are printed to standard error output as preprocessor +**#error** directives, since standard error is expected to be +redirected to a header file. The **#error** directives will cause the +error messages to be printed when the header file is compiled. + +Although it's distributed with the author's Atari 8-bit +utilities, there's nothing Atari-specific about **blob2c**. It could be +useful for any situation where you need to include a file's contents +as an array in a C program. + +EXAMPLE +======= + +:: + + $ echo "Hello, World." > hello.bin + + $ blob2c hello.bin >hello.c 2>hello.h + + ## check exit status (0=success) + $ echo $? + 0 + + $ cat hello.h + /* C header created by blob2c from input file hello.bin */ + + #ifndef hello_bin_H + #define hello_bin_H + + extern unsigned char hello_bin[]; + extern int hello_bin_len; + + #endif /* hello_bin_H */ + + $ cat hello.c + + /* C source created by blob2c from input file hello.bin */ + + unsigned char hello_bin[] = { + /* 0 */ 0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57, /* Hello, W */ + /* 8 */ 0x6f,0x72,0x6c,0x64,0x2e,0x0a /* orld.. */ + }; /* hello_bin */ + + int hello_bin_len = 14; + +.. include:: manftr.rst @@ -0,0 +1,261 @@ +/* CART header support library */ + +#include <string.h> +#include <stdio.h> +#include "cart.h" + +/* 20071227 bkw: + cart_types list was made from atari800-2.0.3/DOC/cart.txt + I didn't use any code from atari800, since it's GPL and this code + is WTFPL. + + Format of CART header (from atari800 cart.txt): + + first 4 bytes containing 'C' 'A' 'R' 'T'. + next 4 bytes containing cartridge type in MSB format (see the table below). + next 4 bytes containing cartridge checksum in MSB format (ROM only). + next 4 bytes are currently unused (zero). + followed immediately with the ROM data: 4, 8, 16, 32, 40, 64, 128, 256, 512 + or 1024 kilobytes. +*/ + +/* 20071227 bkw: this list is complete as of Atari800 2.0.3 */ +/* 20220827 bkw: list now complete as of Atari800 5.0.0 */ +cart_t cart_types[] = { + /* Id */ /* Machine, Size, Name */ + /* 0 */ { M_INVALID, 0, 0 }, /* 0 is invalid type */ + /* 1 */ { M_ATARI8, 8, "Standard 8 KB" }, + /* 2 */ { M_ATARI8, 16, "Standard 16 KB" }, + /* 3 */ { M_ATARI8, 16, "OSS two chip 16 KB cartridge (034M)" }, + /* 4 */ { M_5200, 32, "Standard 32 KB 5200" }, + /* 5 */ { M_ATARI8, 32, "DB 32 KB" }, + /* 6 */ { M_5200, 16, "Two chip 16 KB 5200" }, + /* 7 */ { M_5200, 40, "Bounty Bob Strikes Back 40 KB 5200" }, + /* 8 */ { M_ATARI8, 64, "64 KB Williams" }, + /* 9 */ { M_ATARI8, 64, "Express 64 KB" }, + /* 10 */ { M_ATARI8, 64, "Diamond 64 KB" }, + /* 11 */ { M_ATARI8, 64, "SpartaDos X 64 KB" }, + /* 12 */ { M_ATARI8, 32, "XEGS 32 KB" }, + /* 13 */ { M_ATARI8, 64, "XEGS 64 KB (banks 0-7)" }, + /* 14 */ { M_ATARI8, 128, "XEGS 128 KB" }, + /* 15 */ { M_ATARI8, 16, "OSS one chip 16 KB" }, + /* 16 */ { M_5200, 16, "One chip 16 KB 5200" }, + /* 17 */ { M_ATARI8, 128, "Decoded Atrax 128 KB" }, + /* 18 */ { M_ATARI8, 40, "Bounty Bob Strikes Back 40 KB" }, + /* 19 */ { M_5200, 8, "Standard 8 KB 5200" }, + /* 20 */ { M_5200, 4, "Standard 4 KB 5200" }, + /* 21 */ { M_ATARI8, 8, "Right slot 8 KB" }, + /* 22 */ { M_ATARI8, 32, "32 KB Williams" }, + /* 23 */ { M_ATARI8, 256, "XEGS 256 KB" }, + /* 24 */ { M_ATARI8, 512, "XEGS 512 KB" }, + /* 25 */ { M_ATARI8, 1024, "XEGS 1 MB" }, + /* 26 */ { M_ATARI8, 16, "MegaCart 16 KB" }, + /* 27 */ { M_ATARI8, 32, "MegaCart 32 KB" }, + /* 28 */ { M_ATARI8, 64, "MegaCart 64 KB" }, + /* 29 */ { M_ATARI8, 128, "MegaCart 128 KB" }, + /* 30 */ { M_ATARI8, 256, "MegaCart 256 KB" }, + /* 31 */ { M_ATARI8, 512, "MegaCart 512 KB" }, + /* 32 */ { M_ATARI8, 1024, "MegaCart 1 MB" }, + /* 33 */ { M_ATARI8, 32, "Switchable XEGS 32 KB" }, + /* 34 */ { M_ATARI8, 64, "Switchable XEGS 64 KB" }, + /* 35 */ { M_ATARI8, 128, "Switchable XEGS 128 KB" }, + /* 36 */ { M_ATARI8, 256, "Switchable XEGS 256 KB" }, + /* 37 */ { M_ATARI8, 512, "Switchable XEGS 512 KB" }, + /* 38 */ { M_ATARI8, 1024, "Switchable XEGS 1 MB" }, + /* 39 */ { M_ATARI8, 8, "Phoenix 8 KB" }, + /* 40 */ { M_ATARI8, 16, "Blizzard 16 KB" }, + /* 41 */ { M_ATARI8, 128, "Atarimax 128 KB Flash" }, + /* 42 */ { M_ATARI8, 1024, "Atarimax 1 MB Flash (old)" }, + /* 43 */ { M_ATARI8, 128, "SpartaDos X 128 KB" }, + /* 44 */ { M_ATARI8, 8, "OSS 8 KB" }, + /* 45 */ { M_ATARI8, 16, "OSS two chip 16 KB (043M)" }, + /* 46 */ { M_ATARI8, 4, "Blizzard 4 KB" }, + /* 47 */ { M_ATARI8, 32, "AST 32 KB" }, + /* 48 */ { M_ATARI8, 64, "Atrax SDX 64 KB" }, + /* 49 */ { M_ATARI8, 128, "Atrax SDX 128 KB" }, + /* 50 */ { M_ATARI8, 64, "Turbosoft 64 KB" }, + /* 51 */ { M_ATARI8, 128, "Turbosoft 128 KB" }, + /* 52 */ { M_ATARI8, 32, "Ultracart 32 KB" }, + /* 53 */ { M_ATARI8, 8, "Low bank 8 KB" }, + /* 54 */ { M_ATARI8, 128, "SIC! 128 KB" }, + /* 55 */ { M_ATARI8, 256, "SIC! 256 KB" }, + /* 56 */ { M_ATARI8, 512, "SIC! 512 KB" }, + /* 57 */ { M_ATARI8, 2, "Standard 2 KB" }, + /* 58 */ { M_ATARI8, 4, "Standard 4 KB" }, + /* 59 */ { M_ATARI8, 4, "Right slot 4 KB" }, + /* 60 */ { M_ATARI8, 32, "Blizzard 32 KB" }, + /* 61 */ { M_ATARI8, 2048, "MegaMax 2 MB" }, + /* 62 */ { M_ATARI8,131072, "The!Cart 128 MB" }, + /* 63 */ { M_ATARI8, 4096, "Flash MegaCart 4 MB" }, + /* 64 */ { M_ATARI8, 2048, "MegaCart 2 MB" }, + /* 65 */ { M_ATARI8, 32768, "The!Cart 32 MB" }, + /* 66 */ { M_ATARI8, 65536, "The!Cart 64 MB" }, + /* 67 */ { M_ATARI8, 64, "XEGS 64 KB (banks 8-15)" }, + /* 68 */ { M_ATARI8, 128, "Atrax 128 KB" }, + /* 69 */ { M_ATARI8, 32, "aDawliah 32 KB" }, + /* 70 */ { M_ATARI8, 64, "aDawliah 64 KB" }, + /* 71 */ { M_5200, 64, "Super Cart 64 KB 5200" }, + /* 72 */ { M_5200, 128, "Super Cart 128 KB 5200" }, + /* 73 */ { M_5200, 256, "Super Cart 256 KB 5200" }, + /* 74 */ { M_5200, 512, "Super Cart 512 KB 5200" }, + /* 75 */ { M_ATARI8, 1024, "Atarimax 1 MB Flash (new)" }, +}; + +const int MAX_CART_TYPE = (sizeof(cart_types)/sizeof(cart_t))-1; + +/* helper functions. + get_msb_dword() and put_msb_dword() are written the way they are in + order to avoid endian-ness issues caused by casts or calls to some + endian-aware function like htonl(). +*/ + +static unsigned int get_msb_dword(unsigned char *cart, int offset) { + return + (cart[offset] << 24) | + (cart[offset+1] << 16) | + (cart[offset+2] << 8) | + (cart[offset+3]); +} + +static void put_msb_dword(unsigned char *cart, int offset, unsigned int data) { + int i; + for(i=3; i>=0; i--) { + cart[offset+i] = data & 0xff; + data >>= 8; + } +} + +/* has_cart_signature() returns boolean */ +int has_cart_signature(unsigned char *cart) { + return memcmp(cart, CART_SIGNATURE, 4) == 0; +} + +void cart_dump_header(unsigned char *cart, int calc_cksum) { + char *type = "UNKNOWN"; + unsigned int id = get_cart_type(cart); + unsigned int real_id = get_msb_dword(cart, CART_TYPE_OFFSET); + + if(id) + type = cart_types[id].name; + else + calc_cksum = 0; + + /* Show real type, if get_cart_type() returned zero */ + fprintf(stderr, "CART signature: %s\n", + (has_cart_signature(cart) ? "Present" : "MISSING")); + fprintf(stderr, "Cartridge type: %s (ID %d)\n", type, real_id); + fprintf(stderr, "ROM size: "); + if(id) + fprintf(stderr, "%d bytes + 16 byte header\n", get_cart_size(cart)); + else + fprintf(stderr, "UNKNOWN\n"); + + fprintf(stderr, "Machine type: "); + switch(get_cart_machine(cart)) { + case M_ATARI8: + fprintf(stderr, "Atari 8-bit computer\n"); + break; + + case M_5200: + fprintf(stderr, "Atari 5200\n"); + break; + + default: + fprintf(stderr, "UNKNOWN\n"); + break; + } + + fprintf(stderr, "CART checksum: 0x%08x", get_cart_checksum(cart)); + if(calc_cksum) { + fputs((cart_checksum_ok(cart) ? "OK" : "BAD"), stderr); + } + fputc('\n', stderr); +} + +/* get_cart_type() returns the cartridge ID from the CART header, or zero + if it's an invalid/unknown type. */ +unsigned int get_cart_type(unsigned char *cart) { + unsigned int type = get_msb_dword(cart, CART_TYPE_OFFSET); + + if(type == 0 || type > MAX_CART_TYPE) + return 0; + + return type; +} + +/* get_cart_size() returns the cartridge ROM size in bytes, NOT including + the 16-byte header, or zero if invalid type */ +unsigned int get_cart_size(unsigned char *cart) { + unsigned int type = get_cart_type(cart); + if(type == 0) + return 0; + + return cart_types[type].size * 1024; +} + +/* get_cart_checksum() returns the checksum stored in the header. It does + NOT calculate the checksum (use calc_rom_checksum() for that) */ +unsigned int get_cart_checksum(unsigned char *cart) { + return get_msb_dword(cart, CART_CHECKSUM_OFFSET); +} + +machine_t get_cart_machine(unsigned char *cart) { + unsigned int type = get_cart_type(cart); + if(type == 0) + return M_INVALID; + + return cart_types[type].machine; +} + +/* calc_rom_checksum() is the odd man out: it takes a pointer to the + raw ROM data, NOT to the header+data like all the other functions do! */ +unsigned int calc_rom_checksum(unsigned char *rom, int bytes) { + unsigned int cksum = 0; + + while(bytes-- > 0) cksum += *rom++; + + return cksum; +} + +/* cart_checksum_ok() must be called with a complete .CAR image, not just + a header! */ +unsigned int cart_checksum_ok(unsigned char *cart) { + unsigned int bytes = get_cart_size(cart); + unsigned int got = calc_rom_checksum(cart + 16, bytes); + unsigned int expected = get_cart_checksum(cart); + + return (got == expected); +} + +/* create_cart_header() must be supplied with a buffer, which must be at least + 16 bytes in size. Returns 0 with buffer unchanged if type was invalid. */ +int create_cart_header( + unsigned char *buffer, + unsigned char *rom, + unsigned int type) +{ + if(type == 0 || type > MAX_CART_TYPE) + return 0; + + memcpy(buffer, CART_SIGNATURE, 4); + + set_cart_type(buffer, type); + set_cart_checksum(buffer, + calc_rom_checksum(rom, cart_types[type].size * 1024)); + set_cart_unused(buffer, 0); + + return 1; +} + +void set_cart_checksum(unsigned char *cart, unsigned int sum) { + put_msb_dword(cart, CART_CHECKSUM_OFFSET, sum); +} + +void set_cart_type(unsigned char *cart, unsigned int type) { + put_msb_dword(cart, CART_TYPE_OFFSET, type); +} + +void set_cart_unused(unsigned char *cart, unsigned int data) { + put_msb_dword(cart, CART_UNUSED_OFFSET, data); +} + @@ -0,0 +1,55 @@ + +/* CART header support for cart2xex + + Format of CART header (from atari800 cart.txt): + + first 4 bytes containing 'C' 'A' 'R' 'T'. + next 4 bytes containing cartridge type in MSB format (see the table below). + next 4 bytes containing cartridge checksum in MSB format (ROM only). + next 4 bytes are currently unused (zero). + followed immediately with the ROM data: 4, 8, 16, 32, 40, 64, 128, 256, 512 + or 1024 kilobytes. + + See cart.c for the list of cart types. +*/ + +#ifndef CART_H +#define CART_H + +#define CART_SIGNATURE "CART" + +#define CART_SIGNATURE_OFFSET 0 +#define CART_TYPE_OFFSET 4 +#define CART_CHECKSUM_OFFSET 8 +#define CART_UNUSED_OFFSET 12 + +typedef enum { + M_INVALID, + M_ATARI8, + M_5200 +} machine_t; + +typedef struct { + int machine; + int size; + char *name; +} cart_t; + +extern cart_t cart_types[]; +extern const int MAX_CART_TYPE; + +int has_cart_signature(unsigned char *cart); +void cart_dump_header(unsigned char *cart, int calc_cksum); +unsigned int get_cart_type(unsigned char *cart); +unsigned int get_cart_size(unsigned char *cart); +unsigned int get_cart_checksum(unsigned char *cart); +machine_t get_cart_machine(unsigned char *cart); +unsigned int calc_rom_checksum(unsigned char *rom, int bytes); +unsigned int cart_checksum_ok(unsigned char *cart); +int create_cart_header( + unsigned char *buffer, unsigned char *rom, unsigned int type); +void set_cart_checksum(unsigned char *cart, unsigned int sum); +void set_cart_type(unsigned char *cart, unsigned int type); +void set_cart_unused(unsigned char *cart, unsigned int data); + +#endif diff --git a/cart2xex.1 b/cart2xex.1 new file mode 100644 index 0000000..862982a --- /dev/null +++ b/cart2xex.1 @@ -0,0 +1,247 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "CART2XEX" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +cart2xex \- Convert an Atari 8-bit ROM cartridge image to a binary load file +.\" RST source for cart2xex(1) man page. Convert with: +. +.\" rst2man.py cart2xex.rst > cart2xex.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fBcart2xex\fP [\fI\-cdhn\fP] [\fB\-i\fP \fIaddr\fP] [\fB\-r\fP \fIaddr\fP] [\fB\-p\fP \fIpages\fP] [\fB\-t\fP \fItitle\fP] \fIinfile.rom\fP [\fIoutfile.xex\fP] +.SH DESCRIPTION +.sp +\fBcart2xex\fP creates an Atari executable (XEX/COM/BIN/etc) file from +an 8K or 16K non\-bankswitched cartridge ROM dump. The resulting +executable will (by default) have a title screen that displays +\fBLOADING\fP and the filename while the rest of the file loads. +.sp +Input ROM files may be either raw dumps or Atari800 \fB\&.CAR\fP format. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-c +Check ROM and print info only; do not create an executable. +.TP +.B \-d +Don\(aqt include code to make the Atari reboot when RESET is +pressed. Generally, there\(aqs no advantage to using this option, +as the Atari either locks up or reboots anyway when reset. +.TP +.B \-h +Print help (usage) message and exit. +.TP +.BI \-i \ address +Sets the init address of the executable. Default is to use the +init address in the ROM image, at addresses $BFFE/BFFF. An init +address of zero means "no init address"; in this case, the output +won\(aqt contain an init address at all. Executables created +with this option probably won\(aqt run without further modifica‐ +tions. +.TP +.BI \-l \ address +Sets the load address of the executable. Default is to use the +standard Atari cartridge address ($8000 for a 16K cart, $A000 +for 8K). Executables created with this option \fBwill not run\fP without +further modifications. +.TP +.BI \-r \ address +Sets the run address of the executable. Default is to use the +run address in the ROM image, at addresses $BFFA/BFFB. A run address +of zero means "no run address"; in this case, the output +won\(aqt contain a run address at all. Executables created with +this option probably won\(aqt run without further modifications. +.TP +.B \-R +ROM image is a "right cartridge", meant to be used in the right +slot on an 800. Sets the load address to $8000, equivalent to \fB\-l "$8000"\fP\&. +Very few right cartridges were ever made. \fB\&.CAR\fP images +with type 21 (8K right cartridge) will automatically set this +option. +.TP +.B \-n +Do not prepend the code for the \fBLOADING\fP title screen. Without +the title screen, the Atari\(aqs display will become corrupted during +the loading process, although this doesn\(aqt always cause any +real problems. +.TP +.BI \-p \ pages +Reserve extra memory below RAMTOP, in 256\-byte pages. Maximum +value for \fIpages\fP is 16 for a 16K ROM, or 48 for an 8K ROM. +.TP +.BI \-t \ title +Set the title to be displayed during the \fILOADING\fP screen, up to +20 characters. Default: use the output filename as the title +(minus any extension), or leave the title blank if writing to +standard output. +.UNINDENT +.SH NOTES +.sp +\fBcart2xex\fP can\(aqt handle cartridges that use bankswitching (such +as most Atari XEGS\-era releases, or OSS super carts such as Basic +XL or Action!). Only standard 8K and 16K cartridge images are +usable. For raw dumps, this means the input must be exactly 8192 or +16384 bytes. For \fB\&.CAR\fP images, the image type is read from the +CART header; it must be type 1, 2, or 21 (Standard 8K, 16K, or 8K +right\-slot image, respectively). +.sp +\fIinfile\fP may be \fB\-\fP to read from standard input. \fIoutfile\fP +may be \fB\-\fP to write to standard output. \fBcart2xex\fP +will refuse to write binary data to a terminal. +.sp +If \fIoutfile\fP is omitted, but \fIinfile\fP is provided, +the output filename will be constructed from +\fIinfile\fP by replacing the filename extension with \fI\&.xex\fP, or by +appending \fI\&.xex\fP if there is no extension. +.sp +The \fBLOADING\fP screen consists of around 200 bytes of 6502 object code, +loaded at $6000. +The title (\fB\-t\fP argument, or output filename) will be transformed +so that it will be displayed in green on a GRAPHICS 2 screen. Any +~ (tilde) or ] (right square bracket) characters will be replaced with +spaces, since the tilde doesn\(aqt exist in the ATASCII character set, and +a green ] would be the Atari clear\-screen code (CHR$(125)). +.sp +With the \fB\-n\fP option (suppress \fBLOADING\fP screen), the Atari\(aqs +display will get corrupted during the load. This is because the display +list and screen RAM in use during the load is located in the RAM where the +executable will be loaded, which gets overwritten with code/data. Even +without the title screen, \fBcart2xex\fP emits code that sets RAMTOP +to point below the cartridge area, so the screen corruption shouldn\(aqt +cause any problems for most games (since the first thing they do is set +up their own graphics display). However, languages such as BASIC don\(aqt +necessarily re\-open the E: device. In fact, the first thing BASIC does +is print a READY prompt, which (with \fB\-n\fP) causes it to overwrite +part of its own code, and lock up (since the screen address points to a +location within BASIC). +.sp +Not all cartridges will run correctly if loaded into RAM. In particular, +many commercial games contain "self\-destruct" code which attempts to +write to the cartridge\(aqs own ROM (either by accident or +as a copy\-protection measure). This of course fails when running from +ROM, but will succeed if running from RAM. Converting such an image into +a binload file requires disassembling the code, finding the self\-destruct +sequence, and removing it (the Atari800 built\-in debugger is very handy +for this). If you\(aqre looking for an example of a +self\-destructing ROM, try the Parker Brothers version of Frogger, or +Atari Pac\-Man or Millipede. +.sp +Sometimes, the executable won\(aqt work properly when loaded from a regular +DOS, but will work fine with a game DOS such as Fenders 3\-sector loader +or HiassofT\(aqs MyPicoDOS. This seems to happen because the cart code +assumes that portions of memory will be initialized to zero, but with +DOS loaded, they actually contain DOS\(aqs code and data. The reason they +work with bootloaders is that a bootloader is specifically designed to use as +little memory as possible. An example of this type of ROM image is +Imagic\(aqs Atlantis. +.sp +The opposite is also possible: sometimes the executable will work fine +when loaded from DOS, but will not work when loaded via a bootloader +(or the "Load XEX" option in an emulator). This seems to happen for +carts that want to boot DOS (example: Atari Artist). This isn\(aqt a very +serious problem though: normally if a cartridge allows DOS boot, you will +want to be using DOS with it (e.g. so you can save and load your +drawings in Atari Artist). +.sp +Normally, the load/run/init addresses are best left alone. The \fB\-l\fP, +\fB\-i\fP and +\fB\-r\fP options are included for expert users, who may find them useful +for bypassing "self\-destruct" copy protection code. \fBcart2xex\fP will +print warnings if the user sets the addresses outside the address range +of the cartridge ($8000\-$CFFF for 16K carts, or $A000\-$CFFF for 8K), but +will create the executable anyway (always assume the user knows what he\(aqs +doing). These might also be useful if you want to disassemble the ROM\(aqs +code with a cartridge\-based disassembler (in which case, you probably +need \fB\-i0 \-r0\fP as well, to create a file that doesn\(aqt execute +when loaded). +.sp +Addresses for \fB\-l\fP / \fB\-i\fP / \fB\-r\fP and the +number of pages for \fB\-p\fP +may be given in decimal, +hex prefixed with \fB$\fP (don\(aqt forget to quote it for the shell!), +or hex prefixed with \fB0x\fP\&. Hex digits may be given in upper or lowercase. +.sp +Without the \fB\-p\fP option, the title screen +routine will still lower RAMTOP (location 106) to make room for the image. +Text\-mode cartridges such as BASIC or ASM/ED may need some "breathing +room" between RAMTOP and the start of the cartridge code, because some +versions of the Atari OS contain a bug that causes "clear screen" to +clear an extra 64 bytes past RAMTOP. This isn\(aqt a problem for real ROM +cartridges (since ROM can\(aqt be overwritten), but when running from RAM, +this will clear the first 64 bytes of "cartridge" code! +.sp +Some versions of the Atari OS ROM also contain a bug that causes 800 bytes +of memory above RAMTOP to be scrolled, when scrolling the text window in +graphics modes. When using a real cartridge, this doesn\(aqt hurt anything, +but if running from RAM, the code will be scrambled and the Atari will +lock up. Reserving 4 pages (\fB\-p 4\fP) will avoid this, though of course +it will reduce the amount of free memory available to the cartridge. +.sp +Without the \fB\-d\fP option, the output executable will contain a one\-byte +segment that loads a value of 1 into location $0244 (equivalent to the BASIC +\fBPOKE 580,1\fP command). It\(aqs possible that the cartridge\(aqs code may +reset this location when it runs, so reboot\-on\-RESET may not be 100% +reliable. The \fB\-d\fP option is probably only useful for languages like +BASIC or ASM/ED. +.SH EXIT STATUS +.sp +Exit status is zero for success, non\-zero for failure. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/cart2xex.c b/cart2xex.c new file mode 100644 index 0000000..4a8acb2 --- /dev/null +++ b/cart2xex.c @@ -0,0 +1,583 @@ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <ctype.h> +#include <string.h> +#include <errno.h> + +#include "cart.h" +#include "get_address.h" +#include "loadscreen_bin.h" + +/* + cart2xex 20061018 bkw + updated 20070524 bkw + added atari800 CART format 20071227 bkw + + Quick & dirty utility to convert Atari 8-bit cartridge images + to Atari DOS binary load files (suitable for use with DOS the 2.0S + "L" command, or your favorite 3-sector loader, etc). + + Works on non-bankswitched 8K or 16K cart images. Output is: + - Segment containing code that prints LOADING (suppress with -n) + - 6-byte Atari binary load header + - The entire input + - 4-byte header, followed by the init address (total 6 bytes) + - 4-byte header, followed by the run address (total 6 bytes) + + Without the LOADING screen, the resulting binary is 18 bytes + larger than the input. + + If the cart image isn't exactly 8192 or 16384 bytes long, an error + is printed, and the program exits with status 1. Otherwise, exit + status is 0 (success). + + Future options (stuff I might add, if there's any interest): + + - ATR image output support (image would consist of a 3-sector + loader plus the binary)... not really necessary: can use "makeatr" + to make a k-file if you really want. + + - Simple algorithm to detect STA/STX/STY instructions that write + to the ROM address space. If I were going to copy-protect a cart, + to keep people from dumping it to a bin, I'd write to what's supposed + to be ROM, then read it back. If I got back what I wrote there, it + means the "ROM" is actually RAM, so I'd refuse to play the game. + Parker Bros. Frogger does this. + The trouble is, a *simple* algorithm will probably fail. Several + self-destructing carts I've dealt with use STA ($00),Y to write to + ROM... meaning I basically need a complete emulator. Atari++ has a + nice debugger with conditional breakpoints, maybe there's a way to + hook into that? + + - Documentation for all this crap, if I really add it :) +*/ + +#ifndef VERSION +#define VERSION "???" +#endif + +#define SELF "cart2xex" + +#define BANNER \ + SELF " v" VERSION " - by B. Watson (WTFPL)\n" + +#define MAX_CART_SIZE (16384 + 16) + +char *usage = + "\nUsage: " SELF " [-cdhilnprR] [-t title] infile.rom [outfile.xex]\n" + " -c Check only; do not create xex file\n" + " -d Don't include code to reboot the Atari when RESET is pressed\n" + " -h Print this help\n" + " -i addr Force init address to addr (0 = no init address)\n" + " -r addr Force run address to addr (0 = no run address)\n" + " -l addr Force load address to addr\n" + " -R ROM is right cartridge image (same as -l0x8000)\n" + " -p pages Reserve # of pages below RAMTOP\n" + " -n Do not include code for LOADING screen\n" + " -t title Set title for LOADING screen (default: outfile)\n"; + +int loadscreen = 1; /* true if we're going to prepend a title screen */ +int checkonly = 0; /* true if we're not writing an output file */ +int reserve_pages = 0; /* number of extra pages of RAM to reserve */ +int reboot_reset = 1; /* true if RESET key causes a reboot */ + +/* buffer[] will contain the cart image after it's loaded, which may + or may not include a 16-byte CART header. rom will point to the start + of the actual ROM data (buffer or buffer+16) */ +unsigned char buffer[MAX_CART_SIZE]; +unsigned char *rom; + +/* warn() and die() are like the Perl functions of the same names, kind of */ +void warn(char *msg) { + if(msg) fprintf(stderr, "%s\n", msg); +} + +void die(char *msg) { + warn(msg); + exit(1); +} + +/* cart_image_valid() looks at an atari800-style CART header and returns + true if the cartridge type is 8K or 16K non-bankswitched */ +int cart_image_valid(unsigned char *cart) { + int cart_type; + + cart_dump_header(cart, 0); + + cart_type = get_cart_type(cart); + + /* magic numbers: types 1 and 2 are "Standard 8K" and "Standard 16K", + type 21 is "Standard 8K right slot", which are the only types we + can support. */ + if(cart_type != 1 && cart_type != 2 && cart_type != 21) + return 0; + + /* bad cksum is not a fatal error */ + if(cart_checksum_ok(cart)) + fprintf(stderr, "CART checksum: 0x%08x (OK)\n", get_cart_checksum(cart)); + else + warn("Warning: CART checksum invalid, file may be corrupt"); + + return 1; +} + +/* For now, all the work is done in main() */ +int main(int argc, char **argv) { + int maxres, c, size = 0; + unsigned char init_lo, init_hi, run_lo, run_hi, option, present, ramtop_adj; + int runadr, initadr; + int force_init = -1, force_run = -1, force_load = -1; + char *infile = "-", outfile[4096] = "-"; + FILE *in = stdin, *out = stdout; + char *title = NULL; + + puts(BANNER); + + argc--; argv++; + + while(argc && *argv[0] == '-') { + switch(argv[0][1]) { + case 't': + if(argv[0][2]) { + title = &argv[0][2]; + } else { + argc--; argv++; + if(argc) { + title = &argv[0][0]; + } else { + die(usage); + } + } + break; + + case 'p': + if(argv[0][2]) { + reserve_pages = get_address(NULL, &argv[0][2]); + } else { + argc--; argv++; + if(argc) { + reserve_pages = get_address(NULL, argv[0]); + } else { + die(usage); + } + } + if(reserve_pages < 0) + exit(1); + break; + + case 'R': + force_load = 0x8000; + break; + + case 'l': + if(argv[0][2]) { + force_load = get_address(NULL, &argv[0][2]); + } else { + argc--; argv++; + if(argc) { + force_load = get_address(NULL, argv[0]); + } else { + die(usage); + } + } + if(force_load < 0) + exit(1); + break; + + case 'i': + if(argv[0][2]) { + force_init = get_address(NULL, &argv[0][2]); + } else { + argc--; argv++; + if(argc) { + force_init = get_address(NULL, argv[0]); + } else { + die(usage); + } + } + if(force_init < 0) + exit(1); + break; + + case 'r': + if(argv[0][2]) { + force_run = get_address(NULL, &argv[0][2]); + } else { + argc--; argv++; + if(argc) { + force_run = get_address(NULL, argv[0]); + } else { + die(usage); + } + } + if(force_run < 0) + exit(1); + break; + + case 'c': + checkonly = 1; + break; + + case 'n': + loadscreen = 0; + break; + + case 'd': + reboot_reset = 0; + break; + + case 'h': + printf(usage); + exit(0); + break; + + default: + fprintf(stderr, "Unrecognized option '-%c'\n", argv[0][1]); + die(usage); + break; + } + + argc--; argv++; + } + + if(argc) { + infile = *argv; + argc--; argv++; + } + + if(argc) { + strcpy(outfile, *argv); + argc--; argv++; + } else if(!checkonly && strcmp(infile, "-") != 0) { + char *p; + + strcpy(outfile, infile); + p = strrchr(outfile, '.'); + if(!p) p = outfile + strlen(outfile); + *p = '\0'; + strcat(outfile, ".xex"); + fprintf(stderr, "- Output is '%s'\n", outfile); + + if(strcmp(infile, outfile) == 0) + die("Input and output filenames are the same, aborting!\n"); + } + + if(argc) + die(usage); + + if(strcmp(infile, "-") != 0) { + if( !(in = fopen(infile, "rb")) ) { + perror(infile); + exit(1); + } + } + + while( (c = getc(in)) != EOF && size < MAX_CART_SIZE) { + buffer[size++] = c; + } + + if(has_cart_signature(buffer)) { + int header_size; + + warn("Image type: Atari800 CART format"); + if(!cart_image_valid(buffer)) + die("Unsupported/invalid CART image\n" + "(only 8K/16K non-banked images are supported)"); + + if(get_cart_type(buffer) == 21) { + /* right cartridge */ + force_load = 0x8000; + warn("Forcing load address to $8000 for right-slot cartridge"); + } + + header_size = get_cart_size(buffer); + rom = buffer + 16; + size -= 16; + + if(size != header_size) + die("CART header says image should be %d bytes, but only %d bytes " + "are in the image! Corrupt/damaged CART image?"); + } else { + warn("Image type: Raw dump"); + rom = buffer; + } + + if( !(c == EOF && (size == 8192 || size == 16384)) ) { + if(rom[0] == 0xff && rom[1] == 0xff) { + warn("This looks like an Atari binary load file!"); + } else if(rom[0] == 0x00 && rom[1] == 0x00) { + warn("This looks like an Atari BASIC program!"); + } + + die("Cartridge size must be 8192 or 16384 bytes\n" + "(Are you sure this is a valid Atari 8-bit cartridge image?)"); + } + + /* All this info comes from Mapping the Atari, originally by + Ian Chadwick, now available for free at + http://www.atariarchives.org/mapping/ + */ + + /* Assume we're using a Left Cartridge. Not many Right Cartridges + were ever made... */ + run_lo = rom[size-6]; /* $BFFA */ + run_hi = rom[size-5]; /* $BFFB */ + present = rom[size-4]; /* $BFFC */ + option = rom[size-3]; /* $BFFD */ + init_lo = rom[size-2]; /* $BFFE */ + init_hi = rom[size-1]; /* $BFFF */ + + /* Some helpful messages... decode the cart options */ + if(present) { /* $BFFC */ + fprintf(stderr, + "Hmm, this cart image has $%02X in the \"cartridge present\" " + "byte at $BFFC\n" + "(should be $00). " + "Are you sure it's really a valid image?\n", + present); + } + + fprintf(stderr, "Image size: %dK (addresses $%04X - $%04X)\n", + size/1024, 49152-size, 49152-1); + fprintf(stderr, "Run address: $%02X%02X\n", run_hi, run_lo); + fprintf(stderr, "Init address: $%02X%02X\n", init_hi, init_lo); + fprintf(stderr, "Option byte: $%02X\n", option); + + /* $BFFD, bits 0, 2, 7 */ + fprintf(stderr, "- Cartridge does %sallow disk boot\n", + (option & 1) ? "" : "NOT "); + + fprintf(stderr, "- Cartridge gets initialized, %s\n", + (option & 4) ? + "then started normally" : + "but NOT started (weird)"); + + fprintf(stderr, "Cartridge type: %s\n", + (option & 128) ? + "diagnostic (output binary may not run correctly!)" : + "normal (non-diagnostic)"); + + if(checkonly) + exit(0); + + if(force_load >= 0) { + fprintf(stderr, "-l option: Load address forced to $%04X\n", force_load); + } + + if(force_init >= 0) { + init_lo = rom[size-2] = force_init & 0xff; + init_hi = rom[size-1] = (force_init >> 8) & 0xff; + fprintf(stderr, "-i option: Init address forced to $%04X\n", force_init); + } + + if(force_run >= 0) { + run_lo = rom[size-6] = force_run & 0xff; + run_hi = rom[size-5] = (force_run >> 8) & 0xff; + fprintf(stderr, "-r option: Run address forced to $%04X\n", force_run); + } + + runadr = run_lo + (run_hi << 8); + if(runadr && (runadr < 49152-size || runadr >= 49152)) + warn("Warning: Run address not in cartridge address range"); + + initadr = init_lo + (init_hi << 8); + if(initadr && (initadr < 49152-size || initadr >= 49152)) + warn("Warning: Init address not in cartridge address range"); + + if(size == 8192 && force_load < 0) { + if(runadr >= 0x8000 && runadr < 0xa000) + warn("This may be a \"right cartridge\" ROM, try -R?"); + } + + /* Whew! Finally all checks are done, we can open the output file */ + if(strcmp(outfile, "-") != 0) { + if( !(out = fopen(outfile, "wb")) ) { + perror(outfile); + fclose(in); + exit(1); + } + + if(!title) { + char *p; + + title = outfile; + + /* remove directory if present */ + p = strrchr(title, '/'); + if(p) title = p + 1; + + /* remove extension if present */ + p = strrchr(title, '.'); + if(p) *p = '\0'; + } + + } + + if(isatty(fileno(out))) { + warn("\nStandard output is a terminal, not writing binary data.\n" + "Either redirect to a file or set the output filename."); + die(usage); + } + + maxres = ((0xc000 - size) - 0x7000) >> 8; + if(reserve_pages > maxres) { + fprintf(stderr, "Can't reserve %d pages below RAMTOP. Maximum " + "reserved pages is %d for a %dK ROM.\n", + reserve_pages, maxres, size/1024); + exit(1); + } + + if(reserve_pages) { + fprintf(stderr, "Reserving %d pages below RAMTOP\n", reserve_pages); + } + + ramtop_adj = (size >> 8) + reserve_pages; + + /* Write the loading screen object code, unless suppressed */ + if(loadscreen) { + int i; + char c, d; + + /* loadscreen.bin adjusts RAMTOP to make room for the cart image, + so it needs to know the size. */ + loadscreen_bin[6] = ramtop_adj; + + if(title) { + /* WARNING: magic number here! */ + /* Look for "Title offset: loadscreen_bin_len - $xx" + in dasm output. */ + int offset = loadscreen_bin_len - 27; + int len = strlen(title); + + if(len > 20) { + fprintf(stderr, "Title > 20 characters, truncating\n"); + len = 20; + } + offset += 10-len/2; + + fprintf(stderr, "- Setting title to \""); + for(i=0; i<len; i++) { + c = title[i]; + + c = tolower(c); + d = c; + + if(c >= 33 && c <= 63) { + c -= 32; + } else if(c >= 64 && c <= 95) { + c += 32; + } else if(!isalnum(c)) { + d = c = 32; + } + fputc(d, stderr); + + loadscreen_bin[offset+i] = c; + } + + fputc('"', stderr); + fputc('\n', stderr); + } else { + fprintf(stderr, + "No -t and no output filename, not setting load screen title\n"); + } + + /* loadscreen_bin already includes $FF, $FF header */ + for(i=0; i<loadscreen_bin_len; i++) + putc(loadscreen_bin[i], out); + } else { + /* $FF, $FF binary load header. All Atari executables must have this. */ + putc(0xff, out); + putc(0xff, out); + + /* Without the title screen, we still need to set RAMTOP. Do it + by loading a 1-byte segment at $6A. */ + putc(0x6a, out); + putc(0x00, out); + putc(0x6a, out); + putc(0x00, out); + putc(0xc0 - ramtop_adj, out); /* assume RAMTOP is 48K */ + } + + if(reboot_reset) { + /* make the Atari reboot when user presses RESET (POKE 580,1) */ + putc(0x44, out); /* start addr LSB */ + putc(0x02, out); /* start addr MSB */ + putc(0x44, out); /* end addr LSB */ + putc(0x02, out); /* end addr MSB */ + putc(0x01, out); /* one byte of data */ + } + + /* Now write the Atari binary load file... Remember, the 6502 expects + addresses to be little-endian (LSB, then MSB) */ + + /* First segment: the code. */ + /* Load address (LSB first). $8000 for a 16K cart, or $A000 for 8K, + or force_load if user used it */ + if(force_load >= 0) { + putc(force_load & 0xff, out); + putc(force_load >> 8, out); + force_load += (size - 1); + putc(force_load & 0xff, out); + putc(force_load >> 8, out); + } else { + putc(0, out); + putc(size == 16384 ? 0x80 : 0xa0, out); + /* End address (LSB first). Always $BFFF */ + putc(0xff, out); + putc(0xbf, out); + } + + /* Image data */ + for(c=0; c<size; c++) + putc(rom[c], out); + + /* Only write init address if it's not 0 */ + if(init_hi || init_lo) { + /* Next segment loads at INITAD ($2E2), contains the cart's init addr */ + putc(0xe2, out); + putc(0x02, out); + + /* Segment length is 2 bytes (ends at $2E3) */ + putc(0xe3, out); + putc(0x02, out); + + /* Init address will get loaded at $2E2 and $2E3 */ + putc(init_lo, out); + putc(init_hi, out); + } + + /* Only write run address if it's not 0 */ + /* Next segment loads at RUNAD ($2E0), contains the cart's run address */ + if(run_hi || run_lo) { + putc(0xe0, out); + putc(0x02, out); + + /* Segment length is 2 bytes (ends at $2E1) */ + putc(0xe1, out); + putc(0x02, out); + + /* Run address will get loaded at $2E0 and $2E1 */ + putc(run_lo, out); + putc(run_hi, out); + } + + /* Check for I/O errors before closing files */ + c = 0; + if(ferror(in)) { + fprintf(stderr, "%s: %s\n", infile, strerror(errno)); + c = 1; + } + + if(ferror(out)) { + fprintf(stderr, "%s: %s\n", outfile, strerror(errno)); + c = 1; + } + + /* Close 'em, we're done */ + fclose(in); + fclose(out); + + /* That's all, folks! */ + exit(c); +} diff --git a/cart2xex.rst b/cart2xex.rst new file mode 100644 index 0000000..aff6272 --- /dev/null +++ b/cart2xex.rst @@ -0,0 +1,203 @@ +.. RST source for cart2xex(1) man page. Convert with: +.. rst2man.py cart2xex.rst > cart2xex.1 +.. rst2man.py comes from the SBo development/docutils package. + +======== +cart2xex +======== + +---------------------------------------------------------------- +Convert an Atari 8-bit ROM cartridge image to a binary load file +---------------------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +**cart2xex** [*-cdhn*] [**-i** *addr*] [**-r** *addr*] [**-p** *pages*] [**-t** *title*] *infile.rom* [*outfile.xex*] + +DESCRIPTION +=========== + +**cart2xex** creates an Atari executable (XEX/COM/BIN/etc) file from +an 8K or 16K non-bankswitched cartridge ROM dump. The resulting +executable will (by default) have a title screen that displays +**LOADING** and the filename while the rest of the file loads. + +Input ROM files may be either raw dumps or Atari800 **.CAR** format. + +OPTIONS +======= + +-c + Check ROM and print info only; do not create an executable. + +-d + Don't include code to make the Atari reboot when RESET is + pressed. Generally, there's no advantage to using this option, + as the Atari either locks up or reboots anyway when reset. + +-h + Print help (usage) message and exit. + +-i address + Sets the init address of the executable. Default is to use the + init address in the ROM image, at addresses $BFFE/BFFF. An init + address of zero means "no init address"; in this case, the output + won't contain an init address at all. Executables created + with this option probably won't run without further modifica‐ + tions. + +-l address + Sets the load address of the executable. Default is to use the + standard Atari cartridge address ($8000 for a 16K cart, $A000 + for 8K). Executables created with this option **will not run** without + further modifications. + +-r address + Sets the run address of the executable. Default is to use the + run address in the ROM image, at addresses $BFFA/BFFB. A run address + of zero means "no run address"; in this case, the output + won't contain a run address at all. Executables created with + this option probably won't run without further modifications. + +-R + ROM image is a "right cartridge", meant to be used in the right + slot on an 800. Sets the load address to $8000, equivalent to **-l "$8000"**. + Very few right cartridges were ever made. **.CAR** images + with type 21 (8K right cartridge) will automatically set this + option. + +-n + Do not prepend the code for the **LOADING** title screen. Without + the title screen, the Atari's display will become corrupted during + the loading process, although this doesn't always cause any + real problems. + +-p pages + Reserve extra memory below RAMTOP, in 256-byte pages. Maximum + value for *pages* is 16 for a 16K ROM, or 48 for an 8K ROM. + +-t title + Set the title to be displayed during the *LOADING* screen, up to + 20 characters. Default: use the output filename as the title + (minus any extension), or leave the title blank if writing to + standard output. + +NOTES +===== + +**cart2xex** can't handle cartridges that use bankswitching (such +as most Atari XEGS-era releases, or OSS super carts such as Basic +XL or Action!). Only standard 8K and 16K cartridge images are +usable. For raw dumps, this means the input must be exactly 8192 or +16384 bytes. For **.CAR** images, the image type is read from the +CART header; it must be type 1, 2, or 21 (Standard 8K, 16K, or 8K +right-slot image, respectively). + +*infile* may be **-** to read from standard input. *outfile* +may be **-** to write to standard output. **cart2xex** +will refuse to write binary data to a terminal. + +If *outfile* is omitted, but *infile* is provided, +the output filename will be constructed from +*infile* by replacing the filename extension with *.xex*, or by +appending *.xex* if there is no extension. + +The **LOADING** screen consists of around 200 bytes of 6502 object code, +loaded at $6000. +The title (**-t** argument, or output filename) will be transformed +so that it will be displayed in green on a GRAPHICS 2 screen. Any +~ (tilde) or ] (right square bracket) characters will be replaced with +spaces, since the tilde doesn't exist in the ATASCII character set, and +a green ] would be the Atari clear-screen code (CHR$(125)). + +With the **-n** option (suppress **LOADING** screen), the Atari's +display will get corrupted during the load. This is because the display +list and screen RAM in use during the load is located in the RAM where the +executable will be loaded, which gets overwritten with code/data. Even +without the title screen, **cart2xex** emits code that sets RAMTOP +to point below the cartridge area, so the screen corruption shouldn't +cause any problems for most games (since the first thing they do is set +up their own graphics display). However, languages such as BASIC don't +necessarily re-open the E: device. In fact, the first thing BASIC does +is print a READY prompt, which (with **-n**) causes it to overwrite +part of its own code, and lock up (since the screen address points to a +location within BASIC). + +Not all cartridges will run correctly if loaded into RAM. In particular, +many commercial games contain "self-destruct" code which attempts to +write to the cartridge's own ROM (either by accident or +as a copy-protection measure). This of course fails when running from +ROM, but will succeed if running from RAM. Converting such an image into +a binload file requires disassembling the code, finding the self-destruct +sequence, and removing it (the Atari800 built-in debugger is very handy +for this). If you're looking for an example of a +self-destructing ROM, try the Parker Brothers version of Frogger, or +Atari Pac-Man or Millipede. + +Sometimes, the executable won't work properly when loaded from a regular +DOS, but will work fine with a game DOS such as Fenders 3-sector loader +or HiassofT's MyPicoDOS. This seems to happen because the cart code +assumes that portions of memory will be initialized to zero, but with +DOS loaded, they actually contain DOS's code and data. The reason they +work with bootloaders is that a bootloader is specifically designed to use as +little memory as possible. An example of this type of ROM image is +Imagic's Atlantis. + +The opposite is also possible: sometimes the executable will work fine +when loaded from DOS, but will not work when loaded via a bootloader +(or the "Load XEX" option in an emulator). This seems to happen for +carts that want to boot DOS (example: Atari Artist). This isn't a very +serious problem though: normally if a cartridge allows DOS boot, you will +want to be using DOS with it (e.g. so you can save and load your +drawings in Atari Artist). + +Normally, the load/run/init addresses are best left alone. The **-l**, +**-i** and +**-r** options are included for expert users, who may find them useful +for bypassing "self-destruct" copy protection code. **cart2xex** will +print warnings if the user sets the addresses outside the address range +of the cartridge ($8000-$CFFF for 16K carts, or $A000-$CFFF for 8K), but +will create the executable anyway (always assume the user knows what he's +doing). These might also be useful if you want to disassemble the ROM's +code with a cartridge-based disassembler (in which case, you probably +need **-i0 -r0** as well, to create a file that doesn't execute +when loaded). + +Addresses for **-l** / **-i** / **-r** and the +number of pages for **-p** +may be given in decimal, +hex prefixed with **$** (don't forget to quote it for the shell!), +or hex prefixed with **0x**. Hex digits may be given in upper or lowercase. + +Without the **-p** option, the title screen +routine will still lower RAMTOP (location 106) to make room for the image. +Text-mode cartridges such as BASIC or ASM/ED may need some "breathing +room" between RAMTOP and the start of the cartridge code, because some +versions of the Atari OS contain a bug that causes "clear screen" to +clear an extra 64 bytes past RAMTOP. This isn't a problem for real ROM +cartridges (since ROM can't be overwritten), but when running from RAM, +this will clear the first 64 bytes of "cartridge" code! + +Some versions of the Atari OS ROM also contain a bug that causes 800 bytes +of memory above RAMTOP to be scrolled, when scrolling the text window in +graphics modes. When using a real cartridge, this doesn't hurt anything, +but if running from RAM, the code will be scrambled and the Atari will +lock up. Reserving 4 pages (**-p 4**) will avoid this, though of course +it will reduce the amount of free memory available to the cartridge. + +Without the **-d** option, the output executable will contain a one-byte +segment that loads a value of 1 into location $0244 (equivalent to the BASIC +**POKE 580,1** command). It's possible that the cartridge's code may +reset this location when it runs, so reboot-on-RESET may not be 100% +reliable. The **-d** option is probably only useful for languages like +BASIC or ASM/ED. + +EXIT STATUS +=========== + +Exit status is zero for success, non-zero for failure. + +.. include:: manftr.rst diff --git a/dasm2atasm b/dasm2atasm new file mode 100755 index 0000000..016722f --- /dev/null +++ b/dasm2atasm @@ -0,0 +1,275 @@ +#!/usr/bin/perl -w + +sub usage { + print <<EOF; +Usage: $0 -[aclmr] infile.asm [outfile.m65] +See man page for details. +EOF + exit 1; +} + +sub get_mac_sub { + my $rex = shift; + my $code = "sub { s/($rex)/\\U\$1/gio };"; + #warn "code is $code"; + return eval "$code"; +} + +sub unhex { + # makes a proper $xx, $xx, $xx list of bytes + # from a list of hex digits, spaces optional. + my $bytes = shift; + my $ret = ""; + + $bytes =~ s/\s//g; + + #warn "unhex: bytes is $bytes"; + + for($bytes =~ /(..)/g) { + #warn "unhex: found $_"; + $ret .= "\$$_, "; + } + + chop $ret; + chop $ret; + + return $ret; +} + +sub fix_include { + my $inc = shift; + my $old = $inc; + $inc =~ s/\.(\w+)("?)$/.m65$2/; + + if($recursive) { + system("$cmd $old $inc"); + } else { + warn "Don't forget to convert included file `$old' to .m65 format!\n"; + } + return $inc; +} + +sub do_subs { + # Do the dirty work of the substitutions. Only reason we have this + # as a subroutine of its own is for profiling purposes (and we do + # spend a *lot* of time here!) + my $line = shift; + + for($line) { + s/^(\@?\w+):/$1/; # no colons after labels, in atasm + s/%/~/g; # binary constant + s/!=/<>/g; # inequality + + s/^(\s+)\.?echo(.*)/;;;;;$1.warn$2/i && + do { warn "$in, line $.:\n\t`.warn' not fully compatible with dasm's `echo', commented out\n" } + && next; + + # This is supposed to change e.g. `bpl .label' to `bpl @label' + s/^(\s+)([a-z]{3})(\s+)\.(\w+)/$1$2$3\@$4/i + && next; + + + s/{(\d)}/%$1/g; # macro arg (gotta do this *after* bin. constants!) + +# atasm doesn't support shifts, emulate with multiply/divide + s/\s*<<\s*(\d+)/"*" . 2**$1/eg; + s/\s*>>\s*(\d+)/"\/" . 2**$1/eg; + +# atasm chokes sometimes when there's whitespace around an operator +# unfortunately, a construct like `bne *-1' can't afford to lose the +# space before the *... why, oh why, does * have to be both multiply and +# program counter? *sigh* + +# s/\s*([-!|\/+*&])\s*/$1/g; + +# ARGH. Why does dasm allow `byte #1, #2, #3'... and why do people *use* it?! + s/^(\s+)\.?byte(\s+)/$1.byte$2/i && do { s/#//g } && next; + s/^(\s+)\.?word(\s+)/$1.word$2/i && do { s/#//g } && next; + s/^(\s+)\.?dc\.w(\s+)/$1.word$2/i && do { s/#//g } && next; + s/^(\s+)\.?dc(?:\.b)?(\s+)/$1.byte$2/i && do { s/#//g } && next; + +# 20070529 bkw: turn ".DS foo" into ".DC foo 0" + s/^(\s+)\.?ds(\s+)(\S+)/$1.dc $3 0 /i && do { s/#//g } && next; + +# I really want to add `hex' to atasm. 'til then though, fake with .byte + s/^(\s+)\.?hex\s+(.*)/$1 . '.byte ' . + unhex($2)/ie && next; + + s/^(\s+)\.?subroutine(.*)/$1.local$2/i && next; + s/^(\s+)\.?include(\s+)(.*)/$1 . '.include' . $2 . fix_include($3)/gie + && next; + s/^(\s+)\.?equ\b/$1=/i && next; + s/^(\s+)\.?repeat\b/$1.rept/i && next; + s/^(\s+)\.?repend\b/$1.endr/i && next; + s/^(\s+)\.?endm\b/$1.endm/i && next; + s/^(\s+)\.?org(\s+)([^,]*).*$/$1*=$2$3/i && next; + s/^(\s+)\.?incbin\b/$1\.incbin/i && next; + s/^(\s+)\.?err(.*)/$1.error$2/i && next; # TODO: see if atasm allows `.error' with no message. + s/^(\s+)\.?ifconst\s+(.*)/$1.if .def $2/i + && next; # TODO: test this! + s/^(\s+)\.?else/$1.else/i && next; + s/^(\s+)\.?endif/$1.endif/i && next; + s/^(\s+)\.?if\s+(.*)/$1.if $2/i && next; + + # stuff that doesn't work: + s/^(\s+)(\.?seg(\..)?\s.*)/;;;;; dasm2atasm: `seg' not supported by atasm\n;;;;;$1$2/i + && next; + s/^(\s+)(\.?processor\s.*)/;;;;; dasm2atasm: `processor' not supported by atasm\n;;;;;$1$2/i + && next; + + s/^(\s+)sta\.w(\s+)(.*)/;;;;; dasm2atasm: was `sta.w $3', using .byte to generate opcode\n$1.byte \$8d, <$3, >$3/i + && next; + + s/^(\s+)stx\.w(\s+)(.*)/;;;;; dasm2atasm: was `stx.w $3', using .byte to generate opcode\n$1.byte \$8e, <$3, >$3/i + && next; + + s/^(\s+)sta\.w(\s+)(.*)/;;;;; dasm2atasm: was `sty.w $3', using .byte to generate opcode\n$1.byte \$8c, <$3, >$3/i + && next; + + # atasm lacks `align', so make up for it with a macro + if(s/(\s)\.?align(\s+)(.*)/$1ALIGN$2$3/i) { + if(!$align_defined) { # only gotta define it if not already defined. + for($align_macro) { + $_ =~ s/^/($linenum += 10) . " "/gme if $linenum; + $_ =~ s/\n/\x9b/g if $a8eol; + } + + print OUT $align_macro; # no, I wouldn't use these globals in a CS class assignment. + $align_defined++; + } + next; + } + + # macros. This is by far the biggest pain in the ass yet. + s/(\s)\.?mac\b/$1.macro/i; + if(/(\s)\.macro(\s+)(\w+)/) { + $mac_regex .= "|\\b$3\\b"; + $mac_sub = get_mac_sub($mac_regex); + } + + if(ref $mac_sub) { # if we've found at least one macro so far... + &$mac_sub; # CAPITALIZE everything matching a macro name + } # note: this code assumes macros are *always* defined before they're + # used. atasm requires this, but does dasm? + + } + return $line; +} + +## main() ## + +$ca65 = 0; +$a8eol = 0; +$linenum = 0; +$recursive = 0; + +$cmd = $0; + +while($ARGV[0] =~ /^-/i) { + my $opt = shift; + $cmd .= " $opt"; + + if($opt eq "-c") { + $ca65++; + } elsif($opt eq "-a") { + $a8eol++; + } elsif($opt eq "-l") { + $linenum = 1000; + } elsif($opt eq "-m") { + $a8eol++; + $linenum = 1000; + } elsif($opt eq "-r") { + $recursive++; + } elsif($opt eq "--") { + last; + } else { + warn "Unknown option '$opt'\n"; + usage; + } +} + +if($ca65 && ($linenum || $a8eol)) { + die "Can't use line numbers and/or Atari EOLs with ca65 output\n"; +} + +$align_macro = <<EOF; +;;;;;; ALIGN macro defined by dasm2atasm + .macro ALIGN + *= [[*/%1]+1] * %1 + .endm +EOF + +$align_defined = 0; # we only need to emit the macro definition once. + +$in = shift || usage; +$out = shift; + +($out = $in) =~ s/(\.\w+)?$/.m65/ unless $out; + +die "$0: can't use $in for both input and output\n" if $out eq $in; + +open IN, "<$in" or die "Can't read $in: $!\n"; +open OUT, ">$out" or die "Can't write to $out: $!\n"; + +$hdr = <<EOF; +;;; Converted from DASM syntax with command: +; $cmd $in $out + +EOF + +for($hdr) { + $_ =~ s/^/($linenum += 10) . " "/gme if $linenum; + $_ =~ s/\n/\x9b/g if $a8eol; +} + +print OUT $hdr; + +if($ca65) { + print OUT <<EOF; +;;; ca65 features enabled by dasm2atasm +; To build with ca65: +; ca65 -o foo.o -t none foo.asm +; ld65 -o foo.bin -t none foo.o +.FEATURE pc_assignment +.FEATURE labels_without_colons + +EOF +} + +$mac_regex = "!THIS_ISNT_SUPPOSED_TO_MATCH"; +$mac_sub = ""; # this will be the code ref we call to match $mac_regex + +while(<IN>) { + chomp; + s/\r//; # you might not want this on dos/win, not sure if it matters. + $label = ""; + + if(/^(\w+)\s*=\s*\1/i) { + print OUT ";;;;; dasm2atasm: labels are case-insensitive in atasm\n"; + $line = ";;;;; $_ ; This assignment is an error in atasm"; + next; + } + +# do this before we split out the label: + s/^\./\@/; # local label (dot in dasm, @ in atasm) + + if(s/^([^:;\s]*):?(\s+)/$2/) { + $label = $1; + } + + ($line, $comment) = split /;/, $_, 2; + next unless $line; + + $line = do_subs($line); + +} continue { + if($linenum) { + print OUT "$linenum "; + $linenum += 10; + } + + print OUT $label if $label; + print OUT $line if $line; + print OUT ";$comment" if $comment; + print OUT ($a8eol ? "\x9b" : "\n"); +} diff --git a/dasm2atasm.1 b/dasm2atasm.1 new file mode 100644 index 0000000..c4738ad --- /dev/null +++ b/dasm2atasm.1 @@ -0,0 +1,244 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "DASM2ATASM" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +dasm2atasm \- Convert 6502 assembly source from dasm syntax to atasm or ca65 syntax. +.\" RST source for dasm2atasm(1) man page. Convert with: +. +.\" rst2man.py dasm2atasm.rst > dasm2atasm.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +dasm2atasm \fB\-[aclmr]\fP \fIinfile.dasm\fP [\fIoutfile.m65\fP] +.SH DESCRIPTION +.sp +\fBdasm2atasm\fP tries its best to convert \fBdasm\fP\(aqs syntax into something that +\fBatasm\fP or \fBca65\fP can use. Since \fBatasm\fP\(aqs syntax is 99% compatible with that +of \fBMAC/65\fP, \fBdasm2atasm\fP can be used for that as well. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-a +Atari EOLs. The output will have all UNIX \fB\en\fP characters replaced +with the EOL character \fB0x9b\fP used on the Atari. +.TP +.B \-c +ca65 output. See \fBCA65 NOTES\fP, below. +.TP +.B \-l +Line numbers. Each line in the output file will be numbered, +starting from \fB1000\fP and counting by \fB10\fP\&. +.TP +.B \-m +MAC/65 mode. Shortcut for \fB\-a \-l\fP\&. Output will be suitable for +loading in MAC/65 with the ENTER command (not LOAD!). +.TP +.B \-r +Process include files recursively. This is done by spawning a +new \fBdasm2atasm\fP process for each included file, which is somewhat +resource\-intensive if there are lots of nested include files. +.UNINDENT +.SH NOTES +.sp +\fBdasm2atasm\fP is written in Perl, so it requires a Perl interpreter +to be available at runtime. If your installed perl binary is not located +at \fB/usr/bin/perl\fP, simply edit the \fBdasm2atasm\fP script and +change the location of perl in the first line (the one beginning with +\fI#!/usr/bin/perl\fP). Alternatively, you may run \fBdasm2atasm\fP with +a command like \fBperl dasm2atasm\fP, though the \fB\-r\fP option will +not work correctly in that case. +.sp +There are a few \fBdasm\fP pseudo\-ops that just aren\(aqt present in +\fBatasm\fP: +.INDENT 0.0 +.TP +.B \fIprocessor\fP +\fBdasm\fP supports several target CPUs with different instruction sets, +and requires a \fBprocessor\fP directive in the source code to set the +CPU type. \fBatasm\fP only supports the 6502. \fBdasm2atasm\fP includes +the \fBprocessor\fP directive as a comment, in the output file. +echo +.sp +\fBdasm\fP\(aqs \fBecho\fP directive allows multiple arguments, and can interpolate +values. Example: +.INDENT 7.0 +.INDENT 3.5 +.sp +.nf +.ft C +echo $100\-*, " bytes of zero page left" +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +\fBatasm\fP\(aqs closest equivalent is \fB\&.warn\fP, but it only accepts one +argument, which it treats as a constant string. \fBdasm2atasm\fP includes +all \fBecho\fPs in the input as comments in the output. +.TP +.B \fIseg, seg.u\fP +\fBatasm\fP doesn\(aqt support these at all. \fBdasm2atasm\fP\(aqs output will +include them as comments. +.UNINDENT +.sp +\fIsta.w, sty.w, stx.w\fP +.INDENT 0.0 +.INDENT 3.5 +\fBatasm\fP doesn\(aqt provide a way to force word addressing, when the operand +of a store instruction will allow zero page addressing to be used. You\(aqll +run into this a lot in Atari 2600 code, or any other 6502 code that has to +maintain sync with an external piece of hardware: using word addressing +causes the 6502 to use an extra CPU cycle, which is a commonly used +method of adding a 1\-cycle delay. +.sp +\fBdasm2atasm\fP will convert any such instructions into \fB\&.byte\fP +pseudo\-ops that will generate the correct code. Example: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +;;;;; dasm2atasm: was \(gasta.w COLUPF\(aq, using .byte to generate opcode +\&.byte $8d, <COLUPF, >COLUPF +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +\fBdasm\fP actually supports the \fI\&.w\fP and \fI\&.b\fP extensions to +most instructions and a few pseudo\-ops. \fBdasm2atasm\fP doesn\(aqt handle +this in the general case. +.UNINDENT +.UNINDENT +.sp +\fI\&. (dot) as program counter\fP +.INDENT 0.0 +.INDENT 3.5 +\fBdasm\fP allows the use of either \fB\&.\fP or \fB*\fP for the current +program counter. \fBatasm\fP only allows \fB*\fP\&. \fBdasm2atasm\fP doesn\(aqt +attempt to translate this, as it doesn\(aqt include a full expression parser. +.UNINDENT +.UNINDENT +.sp +\fI( ) (parentheses)\fP +.INDENT 0.0 +.INDENT 3.5 +\fBdasm\fP allows parentheses or square brackets in expressions: +\fI(1+2)*3\fP and \fI[1+2]*3\fP are equivalent. \fBatasm\fP +only allows square brackets. \fBdasm2atasm\fP does not attempt to +translate this currently, though a future version may. +.UNINDENT +.UNINDENT +.sp +\fImacro arguments\fP +.INDENT 0.0 +.INDENT 3.5 +\fBdasm\fP uses \fB{1}\fP, \fB{2}\fP, etc. to refer to macro arguments +within a macro definition. \fBatasm\fP uses \fB$1\fP, \fB$2\fP, etc. +\fBdasm2atasm\fP makes no attempt to translate these. +.UNINDENT +.UNINDENT +.SH CA65 NOTES +.sp +\fBca65\fP output is actually the same as the \fBatasm\fP output, with +the addition of \fB\&.FEATURE pc_assignment\fP and +\fB\&.FEATURE labels_without_colons\fP at the beginning of the source. +.sp +Most Atari source written with \fBdasm\fP is intended to be assembled +with the \fB\-f3\fP option (raw output). The equvalent option for +\fBatasm\fP is \fB\-r\fP, and for \fBca65\fP (and its linker, +\fBld65\fP) it is \fB\-t none\fP\&. +.sp +However, \fBca65\fP\(aqs linker (\fBld65\fP) will not correctly handle +files with multiple \fBORG\fP directives, when using \fB\-t none\fP\&. Example: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +start ORG $0600 ; or *= $0600 in atasm + ; 5 bytes of code here + LDA #1 ; example code, doesn\(aqt do anything useful + STA 0 + RTS + +; dasm or atasm will include 251 bytes of filler here +; (dasm fills with $00 by default; atasm fills with $ff) + +ORG $0700 ; or *= $0700 in atasm + ; 5 more bytes of code here + LDA #2 + STA 1 + RTS +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +With \fBdasm \-f3\fP or \fBatasm \-r\fP, the output will be 261 bytes of +object code. With \fBca65 \-t none\fP and \fBld65 \-t none\fP, the filler +bytes will not be included, and the output will be only 10 bytes long. +The correct solution to this would be to rewrite the code so that it +doesn\(aqt include any Atari\-specific header information (e.g. binary +load headers as data bytes), then use \fB\-t atari\fP to have \fBca65\fP +generate the binary load headers (though as far as the author knows, +\fBca65\fP doesn\(aqt know how to generate other records such as Atari +boot disk or cartridge headers). +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/dasm2atasm.rst b/dasm2atasm.rst new file mode 100644 index 0000000..cc7b582 --- /dev/null +++ b/dasm2atasm.rst @@ -0,0 +1,162 @@ +.. RST source for dasm2atasm(1) man page. Convert with: +.. rst2man.py dasm2atasm.rst > dasm2atasm.1 +.. rst2man.py comes from the SBo development/docutils package. + +========== +dasm2atasm +========== + +---------------------------------------------------------------------- +Convert 6502 assembly source from dasm syntax to atasm or ca65 syntax. +---------------------------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +dasm2atasm **-[aclmr]** *infile.dasm* [*outfile.m65*] + +DESCRIPTION +=========== + +**dasm2atasm** tries its best to convert **dasm**'s syntax into something that +**atasm** or **ca65** can use. Since **atasm**'s syntax is 99% compatible with that +of **MAC/65**, **dasm2atasm** can be used for that as well. + +OPTIONS +======= + +-a + Atari EOLs. The output will have all UNIX **\\n** characters replaced + with the EOL character **0x9b** used on the Atari. + +-c + ca65 output. See **CA65 NOTES**\, below. + +-l + Line numbers. Each line in the output file will be numbered, + starting from **1000** and counting by **10**. + +-m + MAC/65 mode. Shortcut for **-a -l**. Output will be suitable for + loading in MAC/65 with the ENTER command (not LOAD!). + +-r + Process include files recursively. This is done by spawning a + new **dasm2atasm** process for each included file, which is somewhat + resource-intensive if there are lots of nested include files. + +NOTES +===== + +**dasm2atasm** is written in Perl, so it requires a Perl interpreter +to be available at runtime. If your installed perl binary is not located +at **/usr/bin/perl**, simply edit the **dasm2atasm** script and +change the location of perl in the first line (the one beginning with +*#!/usr/bin/perl*). Alternatively, you may run **dasm2atasm** with +a command like **perl dasm2atasm**, though the **-r** option will +not work correctly in that case. + +There are a few **dasm** pseudo-ops that just aren't present in +**atasm**: + +*processor* + **dasm** supports several target CPUs with different instruction sets, + and requires a **processor** directive in the source code to set the + CPU type. **atasm** only supports the 6502. **dasm2atasm** includes + the **processor** directive as a comment, in the output file. + echo + + **dasm**'s **echo** directive allows multiple arguments, and can interpolate + values. Example:: + + echo $100-*, " bytes of zero page left" + + **atasm**'s closest equivalent is **.warn**, but it only accepts one + argument, which it treats as a constant string. **dasm2atasm** includes + all **echo**\s in the input as comments in the output. + +*seg, seg.u* + **atasm** doesn't support these at all. **dasm2atasm**'s output will + include them as comments. + +*sta.w, sty.w, stx.w* + + **atasm** doesn't provide a way to force word addressing, when the operand + of a store instruction will allow zero page addressing to be used. You'll + run into this a lot in Atari 2600 code, or any other 6502 code that has to + maintain sync with an external piece of hardware: using word addressing + causes the 6502 to use an extra CPU cycle, which is a commonly used + method of adding a 1-cycle delay. + + **dasm2atasm** will convert any such instructions into **.byte** + pseudo-ops that will generate the correct code. Example:: + + ;;;;; dasm2atasm: was `sta.w COLUPF', using .byte to generate opcode + .byte $8d, <COLUPF, >COLUPF + + **dasm** actually supports the *.w* and *.b* extensions to + most instructions and a few pseudo-ops. **dasm2atasm** doesn't handle + this in the general case. + +*. (dot) as program counter* + + **dasm** allows the use of either **.** or ***** for the current + program counter. **atasm** only allows *****. **dasm2atasm** doesn't + attempt to translate this, as it doesn't include a full expression parser. + +*( ) (parentheses)* + + **dasm** allows parentheses or square brackets in expressions: + *(1+2)\*3* and *[1+2]\*3* are equivalent. **atasm** + only allows square brackets. **dasm2atasm** does not attempt to + translate this currently, though a future version may. + +*macro arguments* + + **dasm** uses **{1}**, **{2}**, etc. to refer to macro arguments + within a macro definition. **atasm** uses **$1**, **$2**, etc. + **dasm2atasm** makes no attempt to translate these. + +CA65 NOTES +========== + +**ca65** output is actually the same as the **atasm** output, with +the addition of **.FEATURE pc_assignment** and +**.FEATURE labels_without_colons** at the beginning of the source. + +Most Atari source written with **dasm** is intended to be assembled +with the **-f3** option (raw output). The equvalent option for +**atasm** is **-r**, and for **ca65** (and its linker, +**ld65**) it is **-t none**. + +However, **ca65**'s linker (**ld65**) will not correctly handle +files with multiple **ORG** directives, when using **-t none**. Example:: + + start ORG $0600 ; or *= $0600 in atasm + ; 5 bytes of code here + LDA #1 ; example code, doesn't do anything useful + STA 0 + RTS + + ; dasm or atasm will include 251 bytes of filler here + ; (dasm fills with $00 by default; atasm fills with $ff) + + ORG $0700 ; or *= $0700 in atasm + ; 5 more bytes of code here + LDA #2 + STA 1 + RTS + +With **dasm -f3** or **atasm -r**, the output will be 261 bytes of +object code. With **ca65 -t none** and **ld65 -t none**, the filler +bytes will not be included, and the output will be only 10 bytes long. +The correct solution to this would be to rewrite the code so that it +doesn't include any Atari-specific header information (e.g. binary +load headers as data bytes), then use **-t atari** to have **ca65** +generate the binary load headers (though as far as the author knows, +**ca65** doesn't know how to generate other records such as Atari +boot disk or cartridge headers). + +.. include:: manftr.rst diff --git a/equates.inc b/equates.inc new file mode 100644 index 0000000..4fd60c8 --- /dev/null +++ b/equates.inc @@ -0,0 +1,1384 @@ +; +; ATARI 800 EQUATE LISTING +; +; (version 20070530_bkw) +; +; This is a heavily modified copy of Appendix A of the Atari System +; Reference Manual (with much info added from Appendix B, and from +; Mapping the Atari and other sources) +; +; +;This listing is based on the original release of Operating System, +;version A. The vectors shown here were not changed in version B. +;New equates for XL and XE models are included and noted. Changes +;from version B to XL/XE are also noted. +; +;Most of the equate names given below are the official Atari +;names. They are in common use but are not mandatory. + +; This file can be included in your assembly source, but it's also +; got a lot of useful human-readable comments. It's meant to serve as +; a "quick reference" to Atari programmers, particularly ones who use +; a cross-assembler on a UNIX-ish platform and a text editor that can +; use "ctags": + +; $ ctags equates.inc +; $ vim mystuff.dasm + +; While in vim, press ^] (control-right-bracket) while sitting on a label, +; to jump to that label's definition in this file. (Also, you can type +; :tag labelname). You can also do completion on the labels in vim by +; typing part of a label and pressing ^N (or Tab, if you use the +; CleverTab script from vimhelp.org) + +; GNU Emacs and XEmacs also support ctags, but I've never used them, so +; I dunno how to do it. I do know that you need to use "Exuberant" ctags, +; not the ctags that comes with emacs (which doesn't grok 6502 asm). + +; If you're using an Atari assembler instead of a cross-assembler, you +; don't want to use this as-is: all the extra comments make it huge, and +; it'll be either too large for the Atari's memory, or at least will take +; a long time to assemble. You can make a comment-less, Atari-compatible +; version like so: + +; perl -ne 's/;.*//; s/\s*$//; print if $_' < equates.inc > small.inc + +; ...then use a8eol to convert it to ATASCII format. + +; 20061028 bkw: Originally downloaded from: + +; http://atrey.karlin.mff.cuni.cz/~pavel/atari/atrb.html + +; ...and converted to DASM/ATASM/CA65 format. dasm, atasm, and ca65 use +; similar enough syntax that this file can be used as-is with any of +; them. Unfortunately, this means I can't do conditional assembly in this +; file, since the two assemblers use different semantics... macros are +; even less compatible :( + +; If you use ca65, you need this line in your source: + +;; .FEATURE labels_without_colons + +; before including this file, or else run ca65 with +; "--feature labels_without_colons"). + +; 20070529 bkw: updated, added missing GTIA/POKEY/ANTIC equates, +; documented where the shadows are for those GTIA/POKEY/ANTIC/PIA +; registers that have them. Also added a list of error messages and +; explanation of the cassette buffer layout, and organized the C_* +; CIO constants. Made minor modifications to get this file to assemble +; with ATasm as well as DASM. + +; I have added a few missing equates: this file only +; contained OS ROM locations when I got it. +; I added a few from FMS/DOS as well (e.g. RUNAD and INITAD). + +; XL-specific locations in the original file were duplicate labels +; (e.g. PTIMOT was defined as both $1C and $314), which keeps DASM +; from being able to assemble the file. I prefixed the XL/XL versions +; with "XL_" + +; Also, I've prefixed the CIO command and AUX1 constants with C_, since some +; of them conflicted with other labels in the original version. + +; Areas listed as "unmapped" are literally not connected to anything. +; Trying to read from unmapped address space results in reading whatever +; garbage was on the data bus when the read happened. On my 1200XL, this +; generally results in all 1's ($FF or 255). +; In a 400, 600XL or other Atari with less than 48K of RAM, the missing +; RAM address space is also unmapped. + +; Still TODO: +; - Rest of the DOS/FMS equates +; - Mark 800-ony locations with OSB_ +; - ifdef code, so the user can set the machine type (OSB or XL), +; then refer to e.g. PTIMOT and get either OSB_PTIMOT or XL_PTIMOT +; - Split into separate files? I'd rather not (it's only about 1000 lines) + +; This file's mostly intended for new development. It could also be +; useful for porting old ASM/ED or Mac65 code to DASM, but such code +; may need work... you can always assemble it with ATasm, in that case, +; since it's 99.999% source code compatible with Mac65. + +; References to "APPENDIX C" and such are referring to the Atari System +; Reference Manual, a version of which can be found at: + +; http://atrey.karlin.mff.cuni.cz/~pavel/atari/atrtblc.html + +; References to "Mapping" refer to "Mapping the Atari, Revised Edition", +; which can be found at: + +; http://www.atariarchives.org/mapping/index.php + +; I've pasted a few quotes from Mapping into this file; I consider them +; small enough to be covered under the "fair use" provisions of copyright law +; (I am not a lawyer, though). + +; +; +; DEVICE NAMES +; +; +;SCREDT = "E" SCREEN EDITOR +;KBD = "K" KEYBOARD +;DISPLY = "S" DISPLAY +;PRINTR = "P" PRINTER +;CASSET = "C" CASSETTE +;DISK = "D" DISK DRIVE +; +; +; +; STATUS CODES +; +; + +; 20070529 bkw: These are returned as error codes, though various DOSes +; also define their own codes (usually in the range 160-255). +; Errors 2-21 are defined by BASIC. +; Errors 150-154 are defined by the R: (850 or compatible, RS-232) handler. +SUCCES = $01 ; 1 +BRKABT = $80 ; 128 BREAK KEY ABORT +PRVOPN = $82 ; 130 IOCB ALREADY OPEN +NONDEV = $82 ; 130 NONEXISTANT DEVICE +WRONLY = $83 ; 131 OPENED FOR WRITE ONLY +NVALID = $84 ; 132 INVALID COMMAND +NOTOPN = $85 ; 133 DEVICE OR FILE NOT OPEN +BADIOC = $86 ; 134 INVALID IOCB NUMBER +RDONLY = $87 ; 135 OPENED FOR READ ONLY +EOFERR = $88 ; 136 END OF FILE +TRNRCD = $89 ; 137 TRUNCATED RECORD +TIMOUT = $8A ; 138 PERIPHERAL TIME OUT +DNACK = $8B ; 139 DEVICE DOES NOT ACKNOWLEDGE +FRMERR = $8C ; 140 SERIAL BUS FRAMING ERROR +CRSROR = $8D ; 141 CURSOR OUT OF RANGE +OVRRUN = $8E ; 142 SERIAL BUS DATA OVERRUN +CHKERR = $8F ; 143 SERIAL BUS CHECKSUM ERROR +DERROR = $90 ; 144 PERIPHERAL DEVICE ERROR +BADMOD = $91 ; 145 NON EXISTANT SCREEN MODE +FNCNOT = $92 ; 146 FUNCTION NOT IMPLEMENTED +SCRMEM = $93 ; 147 NOT ENOUGH MEMORY FOR SCREEN MODE + +; BASIC error codes (also used by e.g. Basic XL/XE and Turbo BASIC): +;; 2: Insufficient Memory +;; 3: Value Error +;; 4: Too Many Variables +;; 5: String Length Error +;; 6: Out of Data Error +;; 7: Number Greater than 32767 +;; 8: Input Statement Error +;; 9: Array or String DIM Error +;; 10: Argument Stack Overflow +;; 11: Floating Point Overflow or Underflow Error +;; 12: Line Not Found +;; 13: No Matching FOR Statement +;; 14: Line Too Long +;; 15: GOSUB or FOR Line Deleted +;; 16: RETURN Error +;; 17: Garbage Error +;; 18: Invalid String Character +;; 19: LOAD Program Too Long +;; 20: Bad Channel Number +;; 21: LOAD File Error + +; 850/R: error codes: +;; 150: Serial Port Already Open +;; 151: Concurrent Mode Not Enabled +;; 152: Illegal User-Supplied Buffer +;; 153: Active Concurrent Mode Error +;; 154: Concurrent Mode Not Active + +; DOS error codes (DOS 2.0S only; other DOSes may define other errors) +;; 160: Device Number Error +;; 161: Too Many OPEN Files +;; 162: Disk Full +;; 163: Fatal System Error +;; 164: File Number Mismatch +;; 165: Bad File Name +;; 166: POINT Data Length Error +;; 167: File Locked +;; 168: Invalid XIO Command +;; 169: Directory Full +;; 170: File Not Found +;; 171: POINT Invalid +;; 172: DOS 1 File +;; 173: Bad Sector +;; 255: FORMATTING Error (DOS 2.5) + +; +; +; +; +; COMMAND CODES FOR CIO +; +; + +; Command byte goes in ICCOM,x + +;; General-purpose commands: +C_OPEN = $03 ; 3 OPEN (BASIC OPEN) +C_GETREC = $05 ; 5 GET RECORD +C_GETCHR = $07 ; 7 GET BYTE +C_PUTREC = $09 ; 9 WRITE RECORD +C_PUTCHR = $0B ; 11 PUT-BYTE +C_CLOSE = $0C ; 12 +C_STATUS = $0D ; 13 +C_SPECIL = $0E ; 14 BEGINNING OF SPECIAL COMMANDS (aka XIO) +;; Commands for S: device: +C_DRAWLN = $11 ; 17 SCREEN DRAW (BASIC DRAWTO) +C_FILLIN = $12 ; 18 SCREEN FILL +;; Commands for D: device (only when DOS is loaded): +C_RENAME = $20 ; 32 +C_DELETE = $21 ; 33 +C_LOCK = $23 ; 35 +C_UNLOCK = $24 ; 36 +C_POINT = $25 ; 37 +C_NOTE = $26 ; 38 + +; AUX1 modes (ICAX1,x or 2nd parameter of BASIC OPEN command): +C_OPREAD = $04 ; 4 OPEN FOR INPUT +C_OWRITE = $08 ; 8 OPEN FOR OUTPUT +C_APPEND = $09 ; 9 OPEN TO APPEND TO END OF DISK FILE +C_OUPDAT = $0C ; 12 OPEN FOR INPUT AND OUTPUT AT THE SAME TIME +;; D: (DOS) only: +C_OPDIR = $06 ; 6 OPEN TO DISK DIRECTORY +;; S: only: +C_MXDMOD = $10 ; 16 OPEN TO SPLIT SCREEN (MIXED MODE) +C_INSCLR = $20 ; 32 OPEN TO SCREEN BUT DON'T ERASE +;; C: only: +C_NOIRG = $80 ; 128 NO GAP CASSETTE MODE + +;; Command bytes (ICCOM) for the RS-232 (R:) device: +;; +;; Output partial block 32 $20 +;; Control RTS,XMT,DTR 34 $22 +;; Baud, stop bits, word size 36 $24 +;; Translation mode 38 $26 +;; Concurrent mode 40 $28 +;; +;; (see the 850 Interface Manual for details) + + +; SIO command bytes (not part of CIO): +S_DFRMAT = $21 ; 33 FORMAT DISK (RESIDENT DISK HANDLER (RDH)) +S_PTSECT = $50 ; 80 RDH PUT SECTOR +S_GTSECT = $52 ; 82 RDH GET SECTOR +S_DSTAT = $53 ; 83 RDH GET STATUS +S_PSECTV = $57 ; 87 RDH PUT SECTOR AND VERIFY +; Various other SIO commands are supported by different drives + +; 20061028 bkw: CR/EOL not really part of CIO, but useful: +CR = $9B ; 155 CARRIAGE RETURN (EOL) +EOL = CR ; defined in SYSEQU.ASM + +; +IOCBSZ = $10 ; 16 IOCB SIZE +MAXIOC = $80 ; 128 MAX IOCB BLOCK SIZE +IOCBF = $FF ; 255 IOCB FREE +; +LEDGE = $02 ; 2 DEFAULT LEFT MARGIN +REDGE = $27 ; 39 DEFAULT RIGHT MARGIN + +; OS VARIABLES +; +; PAGE 0 +; +LINZBS = $00 ; 0 (800) FOR ORIGINAL DEBUGGER +; $00 0 (XL) RESERVED +NGFLAG = $01 ; 1 (XL) FOR POWER-UP SELF TEST +CASINI = $02 ; 2 +RAMLO = $04 ; 4 POINTER FOR SELF TEST +TRAMSZ = $06 ; 6 TEMPORARY RAM SIZE +TSTDAT = $07 ; 7 TEST DATA +WARMST = $08 ; 8 +BOOTQ = $09 ; 9 SUCCESSFUL BOOT FLAG +; aka BOOT? in the OS source, but some assemblers don't support ? in labels +DOSVEC = $0A ; 10 PROGRAM RUN VECTOR +DOSINI = $0C ; 12 PROGRAM INITIALIZATION +APPMHI = $0E ; 14 DISPLAY LOW LIMIT +POKMSK = $10 ; 16 IRQ ENABLE FLAGS (shadow for IRQEN) +BRKKEY = $11 ; 17 FLAG +RTCLOK = $12 ; 18 3 BYTES, MSB FIRST +BUFADR = $15 ; 21 INDIRECT BUFFER ADDRESS +ICCOMT = $17 ; 23 COMMAND FOR VECTOR +DSKFMS = $18 ; 24 DISK FILE MANAGER POINTER +DSKUTL = $1A ; 26 DISK UTILITY POINTER (DUP.SYS) +PTIMOT = $1C ; 28 (800) PRINTER TIME OUT REGISTER +ABUFPT = $1C ; 28 (XL) RESERVED +PBPNT = $1D ; 29 (800) PRINTER BUFFER POINTER +; $1D ; 29 (XL) RESERVED +PBUFSZ = $1E ; 30 (800) PRINTER BUFFER SIZE +; $1E ; 30 (XL) RESERVED +PTEMP = $1F ; 31 (800) TEMPORARY REGISTER (PTEMP deleted in XL OS) +; $1F ; 31 (XL) RESERVED +ZIOCB = $20 ; 32 ZERO PAGE IOCB +ICHIDZ = $20 ; 32 HANDLER INDEX NUMBER (ID) +ICDNOZ = $21 ; 33 DEVICE NUMBER +ICCOMZ = $22 ; 34 COMMAND +ICSTAZ = $23 ; 35 STATUS +ICBALZ = $24 ; 36 BUFFER POINTER LOW BYTE +ICBAHZ = $25 ; 37 BUFFER POINTER HIGH BYTE +ICPTLZ = $26 ; 38 PUT ROUTINE POINTER LOW +ICPTHZ = $27 ; 39 PUT ROUTINE POINTER HIGH +ICBLLZ = $28 ; 40 BUFFER LENGTH LOW +ICBLHZ = $29 ; 41 +ICAX1Z = $2A ; 42 AUXILIARY INFORMATION BYTE 1 +ICAX2Z = $2B ; 43 +ICSPRZ = $2C ; 44 TWO SPARE BYTES (CIO USE) +ICIDNO = $2E ; 46 IOCB NUMBER X 16 +CIOCHR = $2F ; 47 CHARACTER BYTE FOR CURRENT OPERATION +; +STATUS = $30 ; 48 STATUS STORAGE +CHKSUM = $31 ; 49 SUM WITH CARRY ADDED BACK +BUFRLO = $32 ; 50 DATA BUFFER LOW BYTE +BUFRHI = $33 ; 51 +BFENLO = $34 ; 52 ADDRESS OF LAST BUFFER BYTE +1 (LOW) +BFENHI = $35 ; 53 +CRETRY = $36 ; 54 (800) NUMBER OF COMMAND FRAME RETRIES +XL_LTEMP = $36 ; 54 (XL) LOADER TEMPORARY STORAGE, 2 BYTES +DRETRY = $37 ; 55 (800) DEVICE RETRIES +BUFRFL = $38 ; 56 BUFFER FULL FLAG +RECVDN = $39 ; 57 RECEIVE DONE FLAG +XMTDON = $3A ; 58 TRANSMISSION DONE FLAG +CHKSNT = $3B ; 59 CHECKSUM-SENT FLAG +NOCKSM = $3C ; 60 CHECKSUM-DOES-NOT-FOLLOW-DATA FLAG +BPTR = $3D ; 61 +FTYPE = $3E ; 62 +FEOF = $3F ; 63 +FREQ = $40 ; 64 +; +SOUNDR = $41 ; 65 0=QUIET I/O +CRITIC = $42 ; 66 CRITICAL FUNCTION FLAG, NO DEFFERED VBI +FMSZPG = $43 ; 67 DOS ZERO PAGE, 7 BYTES +CKEY = $4A ; 74 (800) START KEY FLAG +XL_ZCHAIN = $4A ; 74 (XL) HANDLER LOADER TEMP, 2 BYTES +CASSBT = $4B ; 75 (800) CASSETTE BOOT FLAG +DSTAT = $4C ; 76 DISPLAY STATUS +; +ATRACT = $4D ; 77 +DRKMSK = $4E ; 78 ATTRACT MASK +COLRSH = $4F ; 79 ATTRACT COLOR SHIFTER (EORed WITH GRAPHICS) +; +TMPCHR = $50 ; 80 +HOLD1 = $51 ; 81 +LMARGN = $52 ; 82 SCREEN LEFT MARGIN REGISTER +RMARGN = $53 ; 83 SCREEN RIGHT MARGIN +ROWCRS = $54 ; 84 CURSOR ROW +COLCRS = $55 ; 85 CURSOR COLUMN, 2 BYTES +DINDEX = $57 ; 87 DISPLAY MODE +SAVMSC = $58 ; 88 SCREEN ADDRESS +OLDROW = $5A ; 90 CURSOR BEFORE DRAW OR FILL +OLDCOL = $5B ; 91 +OLDCHR = $5D ; 93 DATA UNDER CURSOR +OLDADR = $5E ; 94 CURSOR ADDRESS +XL_FKDEF = $60 ; 96 (XL) FUNCTION KEY DEFINITION POINTER (LSB/MSB) +NEWROW = $60 ; 96 (800) DRAWTO DESTINATION +NEWCOL = $61 ; 97 (800) DRAWTO DESTINATION, 2 BYTES +XL_PALNTS = $62 ; 98 (XL) EUROPE/NORTH AMERICA TV FLAG +LOGCOL = $63 ; 99 LOGICAL LINE COLUMN POINTER +MLTTMP = $66 ; 102 +OPNTMP = $66 ; 102 TEMPORARY STORAGE FOR CHANNEL OPEN +SAVADR = $68 ; 104 +RAMTOP = $6A ; 106 START OF ROM (END OF RAM + 1), HIGH BYTE ONLY +BUFCNT = $6B ; 107 BUFFER COUNT +BUFSTR = $6C ; 108 POINTER USED BY EDITOR +BITMSK = $6E ; 110 POINTER USED BY EDITOR +SHFAMT = $6F ; 111 +ROWAC = $70 ; 112 +COLAC = $72 ; 114 +ENDPT = $74 ; 116 +DELTAR = $76 ; 118 +DELTAC = $77 ; 119 +ROWINC = $79 ; 121 (800) +XL_KEYDEF = $79 ; 121 (XL) KEY DEFINITION POINTER, 2 BYTES +COLINC = $7A ; 122 (800) +SWPFLG = $7B ; 123 NON 0 IF TEXT AND REGULAR RAM IS SWAPPED +HOLDCH = $7C ; 124 CH MOVED HERE BEFORE CTRL AND SHIFT +INSDAT = $7D ; 125 used by S: handler, tmp for char under cursor +COUNTR = $7E ; 126 used by XIO DRAW command (2 bytes) + +; $80 to $FF are free if BASIC and floating point are not used. +; If BASIC is not used, but FP is, $80 to $D0 are still free. +; There is no way to use BASIC without constantly using FP, as all BASIC +; numbers are FP (even "integers" such as line numbers). +ZROFRE = $80 ; 128 FREE ZERO PAGE, 84 BYTES + +; BASIC zero page variables: +LOMEM = $80 ; 128 LSB, BASIC start-of-memory pointer +; $81 ; 129 MSB, LOMEM (not to be confused with the OS's MEMLO!) +VNTP = $82 ; 130 LSB, BASIC start of Variable Name Table pointer +; $83 ; 131 MSB, VNTP +VNTD = $84 ; 132 LSB, BASIC end of Variable Name Table pointer (+1 byte) +; $85 ; 133 MSB, VNTP +VVTP = $86 ; 134 LSB, BASIC start of Variable Value Table pointer +; $87 ; 135 MSB, VVTP +STMTAB = $88 ; 136 LSB, BASIC start of Statement Table pointer +; $89 ; 137 MSB, STMTAB +STMCUR = $8A ; 138 LSB, BASIC current statement pointer +; $8B ; 139 MSB, STMCUR +STARP = $8C ; 140 LSB, BASIC current string/array table pointer +; $8D ; 141 MSB, STARP (also points to end of BASIC program) +RUNSTK = $8E ; 142 LSB, BASIC runtime stack pointer +; $8F ; 143 MSG, RUNSTK +; BASIC and the OS both use the name MEMTOP; I've renamed the BASIC one. +BAS_MEMTOP = $90 ; 144 LSB, pointer to top of BASIC memory +; $91 ; 145 MSB, BAS_MEMTOP +MEOLFLG = $92 ; 146 "modified EOL flag register", whatever that is +; $93 ; 147 listed as "spare" by Mapping's Errata +;COX = $94 ; 148 current output index (?) +POKADR = $95 ; 149 LSB, address of last POKE location +; ; 150 MSB, POKADR + +; Locations $96 to $B5 are used for various purposes by BASIC, +; and most of them are of little or no interest, even for someone +; writing assembly code meant to run as a USR() routine, so I haven't +; bothered listing them all here. See Compute! Books' "Atari BASIC Sourcebook" +; for the gory details. In fact, you can see it here: + +; http://users.telenet.be/kim1-6502/6502/absb.html + +; It's fascinating (at least it is to me)... includes full source code +; to Atari BASIC! + +; DATAD and DATALN are reset to 0 by BASIC RESTORE command. +DATAD = $B6 ; 182 the data element being read (e.g. 10 for 10th item + ; in a DATA statement) +DATALN = $B7 ; 183 LSB current DATA statement line number +; $B8 ; 184 MSB, DATALN +;ERRNUM = $B9 ; 185 Most recent error number. Gets cleared before you + ; can PEEK it; use ERRSAVE instead. +STOPLN = $BA ; 186 LSB, line where a program stopped by STOP/break/error +; $BB ; 187 MSB, STOPLN +; what are $BC and $BD for? +SAVCUR = $BE ; 190 Saves the current line address (LSB?) +; $BF ; 191 presumably, the MSB of SAVCUR? +IOCMD = $C0 ; 192, I/O Command (Mapping Errata) +IODVC = $C1 ; 193, I/O Device (Mapping Errata) +PROMPT = $C2 ; 194, Prompt character (Mapping Errata, presumably INPUT?) +ERRSAVE = $C3 ; 195 Error code that caused a stop or TRAP +;TEMPA = $C4 ; 196 a 2-byte temp +;ZTEMP2 = $C6 ; 198 a 2-byte temp +COLOR = $C8 ; 200 Stores color from COLOR command +PTABW = $C9 ; 201 Number of columns between tab stops + ; (for PRINT with commas, not the TAB key) +LOADFLG = $CA ; 202 Load in progress flag. I can tell you from bitter + ; experience that BASIC clears this often. + +; $CB - $CF are unused by BASIC or the ASM/ED cart. +; $D0 and $D1 are unused by BASIC (does that mean they *are* used by ASM/ED?) + +; $D2 and $D3 are used by BASIC. Mapping Errata calls them the "BASIC +; floating-point work area". They get cleared to 0 by BASIC, probably +; every time a FP number is used (e.g. "POKE 210,1:? PEEK(210)" prints 0). +; The BASIC source code labels $D2 as TVTYPE and VTYPE, and $D3 as +; TVNUM and VNUM. + +; Floating point zero page variables: +FPZRO = $D4 ; 212 FLOATING POINT RAM, 43 BYTES + ; (20070530 bkw: pretty sure that comment is wrong, and + ; should read 44 bytes; see $FF below) +FR0 = $D4 ; 212 FP REGISTER 0 (also used by BASIC for USR() return val) + ; (FR0/FRE/FR1/FR2 are each 6 bytes long) +FRE = $DA ; 218 +FR1 = $E0 ; 224 FP REGISTER 1 +FR2 = $E6 ; 230 FP REGISTER 2 +FRX = $EC ; 236 SPARE +EEXP = $ED ; 237 VALUE OF E +NSIGN = $ED ; 237 SIGN OF FP NUMBER +ESIGN = $EF ; 239 SIGN OF FP EXPONENT +FCHFLG = $F0 ; 240 FIRST CHARACTER FLAG +DIGRT = $F1 ; 241 NUMBER OF DIGITS RIGHT OF DECIMAL POINT +CIX = $F2 ; 242 INPUT INDEX +INBUFF = $F3 ; 243 POINTER TO ASCII FP NUMBER +ZTEMP1 = $F5 ; 245 +ZTEMP4 = $F7 ; 247 +ZTEMP3 = $F9 ; 249 +DEGFLG = $FB ; 251 +RADFLG = $FB ; 251 0=RADIANS, 6=DEGREES +FLPTR = $FC ; 252 POINTER TO BCD FP NUMBER (2 bytes) +FPTR2 = $FE ; 254 maybe a 2nd pointer to an FP number? (2 bytes) +; $FF ; 255 This *definitely* is used by the FP package + ; Try: POKE 255,0:? SIN(1):? PEEK(255) + +; +; PAGE 1 +; +; 65O2 STACK +; +; + +; +; +; PAGE 2 +; +; +; 20070529 bkw: Bytes listed as "spare" should NOT be used for your own +; purposes. They may not really be unused (just undocumented), and/or they +; may be unused on the 800 but not the XL (or vice versa). +INTABS = $0200 ; 512 INTERRUPT RAM +VDSLST = $0200 ; 512 NMI VECTOR +VPRCED = $0202 ; 514 PROCEED LINE IRQ VECTOR +VINTER = $0204 ; 516 INTERRUPT LINE IRQ VECTOR +VBREAK = $0206 ; 518 break key IRQ vector (not in OS rev. A) +VKEYBD = $0208 ; 520 keyboard IRQ vector (not break/console keys) +VSERIN = $020A ; 522 SERIAL INPUT READY IRQ +VSEROR = $020C ; 524 SERIAL OUTPUT READY IRQ +VSEROC = $020E ; 526 SERIAL OUTPUT COMPLETE IRQ +VTIMR1 = $0210 ; 528 TIMER 1 IRQ vector +VTIMR2 = $0212 ; 530 TIMER 2 IRQ vector +VTIMR4 = $0214 ; 532 TIMER 4 IRQ vector +VIMIRQ = $0216 ; 534 IRQ VECTOR +CDTMV1 = $0218 ; 536 COUNTDOWN TIMER 1 vector +CDTMV2 = $021A ; 538 COUNTDOWN TIMER 2 vector +CDTMV3 = $021C ; 540 COUNTDOWN TIMER 3 vector +CDTMV4 = $021E ; 542 COUNTDOWN TIMER 4 vector +CDTMV5 = $0220 ; 544 COUNTDOWN TIMER 5 vector +VVBLKI = $0222 ; 546 immediate VBLANK vector +VVBLKD = $0224 ; 548 deferred VBLANK vector (ignore if CRITIC != 0) +CDTMA1 = $0226 ; 550 COUNTDOWN TIMER 1 JSR ADDRESS +CDTMA2 = $0228 ; 552 COUNTDOWN TIMER 2 JSR ADDRESS +CDTMF3 = $022A ; 554 COUNTDOWN TIMER 3 FLAG +SRTIMR = $022B ; 555 REPEAT TIMER +CDTMF4 = $022C ; 556 COUNTDOWN TIMER 4 FLAG +INTEMP = $022D ; 557 IAN'S TEMP (used by SETVBL routine) +CDTMF5 = $022E ; 558 COUNTDOWN TIMER FLAG 5 +SDMCTL = $022F ; 559 DMACTL SHADOW +SDLSTL = $0230 ; 560 DISPLAY LIST POINTER, LSB (shadow for DLISTL) +SDLSTH = $0231 ; 561 display list pointer, MSB (shadow for DLISTH) +SSKCTL = $0232 ; 562 SKCTL SHADOW +; $0233 ; 563 (800) UNLISTED (Mapping calls this SPARE) +XL_LCOUNT = $0233 ; 563 (XL) LOADER TEMP +LPENH = $0234 ; 564 LIGHT PEN HORIZONTAL (shadow for PENH) +LPENV = $0235 ; 565 LIGHT PEN VERTICAL (shadow for PENV) +; $0236 ; 566 2 SPARE BYTES +; $0238 ; 568 (800) SPARE, 2 BYTES +;XL_RELADR = $0238 ; 568 (XL) relocatable loader relative addr, 1200XL only! +XL_VPIRQ = $0238 ; 568 (XL) PBI IRQ vector (not on 1200XL!) +CDEVIC = $023A ; 570 DEVICE COMMAND FRAME BUFFER +CAUX1 = $023C ; 572 DEVICE COMMAND AUX 1 +CAUX2 = $023D ; 573 DEVICE COMMAND AUX 2 +TEMP = $023E ; 574 TEMPORARY STORAGE +ERRFLG = $023F ; 575 DEVICE ERROR FLAG (EXCEPT TIMEOUT) +DFLAGS = $0240 ; 576 FLAGS FROM DISK SECTOR 1 +DBSECT = $0241 ; 577 NUMBER OF BOOT DISK SECTORS +BOOTAD = $0242 ; 578 BOOT LOAD ADDRESS POINTER +COLDST = $0244 ; 580 COLD START FLAG, 1 = COLD START IN PROGRESS +; $0245 ; 581 (800) SPARE +XL_RECLEN = $0245 ; 581 (XL) LOADER +DSKTIM = $0246 ; 582 (800) DISK TIME OUT REGISTER +; $0246 ; 582 (XL) RESERVED, 39 BYTES +LINBUF = $0247 ; 583 (800) CHARACTER LINE BUFFER, 40 BYTES + ; LINBUF was deleted from the XL OS and replaced with: + +; $0247 - $024D are "reserved" on the 1200XL. On other XL's they are: +XL_PDVMSK = $0247 ; 583 shadow for PBI device selection register @ $D1FF +XL_SHPDVS = $0248 ; 584 shadow for PBI register (where??) +XL_PDMSK = $0249 ; 585 PBI interrupt mask +XL_RELADR = $024A ; 586 (XL) LSB, relocatable loader relative addr (NOT 1200XL) +; $024B ; 587 MSB, XL_RELADR +XL_PPTMPA = $024C ; 588 temporaries for relocatable loader +XL_PPTMPX = $024D ; 589 " + +; $024E - $026A are "spare" on all XL/XE's + +; More XL stuff: +XL_CHSALT = $026B ; 619 (XL) CHARACTER SET POINTER (ctrl-F4 on 1200XL) +XL_VSFLAG = $026C ; 620 (XL) FINE SCROLL TEMPORARY +XL_KEYDIS = $026D ; 621 (XL) KEYBOARD DISABLE (ctrl-F1 on 1200XL) +XL_FINE = $026E ; 622 (XL) FINE SCROLL FLAG (POKE 622,255:GR.0) + +GPRIOR = $026F ; 623 P/M PRIORITY AND GTIA MODES (shadow for PRIOR) +;GTIA = $026F ; 623 ; 20070529 bkw: does anyone define this? + +; Game controller shadows (joysticks/paddles) +; Joystick directions and paddle triggers (buttons) are wired to the PIA. +; Joystick triggers (fire buttons) and the actual paddle potentiometers +; are wired to the GTIA. +; If this seems a little odd, that's because it is :) + +; Paddles (potentiometers): +PADDL0 = $0270 ; 624 (XL) 3 MORE PADDLES, (800) 7 MORE PADDLES +PADDL1 = $0271 ; 625 (these are read in BASIC with PADDLE(x) +PADDL2 = $0272 ; 626 (PADDL0-7 are shadows for POT0-7) +PADDL3 = $0273 ; 627 +PADDL4 = $0274 ; 628 (PADDL4-7 are copies of PADDL0-3 on the XL) +PADDL5 = $0275 ; 629 +PADDL6 = $0276 ; 630 +PADDL7 = $0277 ; 631 + +; Joysticks (directions only) +STICK0 = $0278 ; 632 (XL) 1 MORE STICK, (800) 3 MORE STICKS +STICK1 = $0279 ; 633 (these are read in BASIC with STICK(x) +STICK2 = $027A ; 634 (STICK0/1 are shadows for PORTA; STICK2/3 shadows PORTB) +STICK3 = $027B ; 635 +; STICK0 is a shadow for bits 4-7 of PORTA (shifted 4 bits right) +; STICK1 is a shadow for bits 0-3 of PORTA + +; On the 800: +; STICK2 is a shadow for bits 4-7 of PORTB (shifted 4 bits right) +; STICK3 is a shadow for bits 0-3 of PORTB + +; On the XL/XE series: +; STICK2 and STICK3 are copies of STICK0 and STICK1, respectively. + +; In the XL/XE machines, there are only 2 joystick ports, and PORTB +; (formerly joystick ports) is now used to control the MMU. + +; joystick directions are active low (1=not pressed) and decode as: + +; bit direction +; 0 or 4 up +; 1 or 5 down +; 2 or 6 left +; 3 or 7 right + +; A value of $0F in a STICKx register means no direction is being pressed. +; When a direction is pressed, its bit becomes a logic 0, so e.g. $0E means +; someone's moving the joystick up. + +; (bits 4-7 are only used when reading directly from the HW registers, +; PORTA and PORTB). + +; Paddle triggers (buttons) +PTRIG0 = $027C ; 636 (XL) 3 MORE PADDLE TRIGGERS, (800) 7 MORE +PTRIG1 = $027D ; 637 (these are read in BASIC with PTRIG(x)) +PTRIG2 = $027E ; 638 (PTRIG0-3 are shadows for PORTA) +PTRIG3 = $027F ; 639 +PTRIG4 = $0280 ; 640 (PTRIG4-7 are shadows for PORTB on the 800) +PTRIG5 = $0281 ; 641 (they are copies of PTRIG0-3 on the XL) +PTRIG6 = $0282 ; 642 +PTRIG7 = $0283 ; 643 +; In case someone doesn't already know this: The paddle triggers are wired +; to the same pins on the joystick port as the left/right joystick directions. +; Each pair of paddles uses left for the first paddle's trigger and right +; for the second (so PTRIG0/1 are also the left/right bits in STICK0, +; PTRIG2/3 are STICK1, etc). + +; Joystick triggers (buttons) +STRIG0 = $0284 ; 644 (XL) 1 MORE STICK TRIGGER, (800) 3 MORE +STRIG1 = $0285 ; 645 (these are read in BASIC with STRIG(x)) +STRIG2 = $0286 ; 646 (STRIG0-3 are shadows for TRIG0-3) +STRIG3 = $0287 ; 647 + +; C: handler variables: +CSTAT = $0288 ; 648 (800) Cassette status register +; note that CSTAT was deleted from the XL OS, and replaced with: +XL_HIBYTE = $0288 ; 648 (XL) used by relocatable loader +WMODE = $0289 ; 649 used by C: handler (0=read, 128-write) +BLIM = $028A ; 650 cassette buffer data record size +; $028B ; 651 (800) 5 SPARE BYTES (to $028F) +XL_IMASK = $028B ; 651 (XL) used by relocatable loader +XL_JVECK = $028C ; 652 (XL) (Mapping says it's unused) + ; 653 (XL) Presumably the MSB of JVECK (unused?) +XL_NEWADR = $028E ; 654 (XL) LOADER RAM (2 bytes) + +; Misc. S: and/or E: handler variables: +TXTROW = $0290 ; 656 +TXTCOL = $0291 ; 657 +TINDEX = $0293 ; 659 TEXT INDEX +TXTMSC = $0294 ; 660 +TXTOLD = $0296 ; 662 OLD ROW AND OLD COL FOR TEXT, 2 BYTES +; $0298 ; 664 4 SPARE BYTES +TMPX1 = $029C ; 668 (800) +; note that TMPX1 was deleted from the XL OS, and replaced with: +XL_CRETRY = $029C ; 668 (XL) NUMBER OF COMMAND FRAME RETRIES + ; (moved from CRETRY on 800) +SUBTMP = $029E ; 670 +HOLD2 = $029F ; 671 +DMASK = $02A0 ; 672 +TMPLBT = $02A1 ; 673 +ESCFLG = $02A2 ; 674 +TABMAP = $02A3 ; 675 15 BYTE BIT MAP FOR TAB SETTINGS +LOGMAP = $02B2 ; 690 4 BYTE LOGICAL LINE START BIT MAP +INVFLG = $02B6 ; 694 mask for inverse video ($80=inverse, 0=normal) +FILFLG = $02B7 ; 695 FILL DURING DRAW FLAG +TMPROW = $02B8 ; 696 +TMPCOL = $02B9 ; 697 +SCRFLG = $02BB ; 699 SCROLL FLAG +HOLD4 = $02BC ; 700 +HOLD5 = $02BD ; 701 (800) +; note that HOLD5 was deleted from the XL OS, and replaced with: +XL_DRETRY = $02BD ; 701 (XL) NUMBER OF DEVICE RETRIES + ; (moved from DRETRY on 800) +SHFLOC = $02BE ; 702 +BOTSCR = $02BF ; 703 24 NORM, 4 SPLIT + +; Color register shadows (HW registers are in GTIA) +PCOLR0 = $02C0 ; 704 3 MORE PLAYER COLOR REGISTERS (shadows for COLPM0-3) +PCOLR1 = $02C1 ; 705 (missiles use same color regs as same-numbered players!) +PCOLR2 = $02C2 ; 706 +PCOLR3 = $02C3 ; 707 +COLOR0 = $02C4 ; 708 4 MORE GRAPHICS COLOR REGISTERS (shadows for COLPF0-3) +COLOR1 = $02C5 ; 709 (text luminance in GR.0) +COLOR2 = $02C6 ; 710 (text background and chroma in GR.0) +COLOR3 = $02C7 ; 711 +COLOR4 = $02C8 ; 712 (background, shadow for COLBK) +; On boot, system reset, or any time S:/E: devices are opened: +; PCOLR0-3 are initialzed to 0 ($00, black) +; COLOR0 is initialized to 40 ($28, orange) +; COLOR1 is initialized to 202 ($CA, green) +; COLOR2 is initialized to 148 ($94, blue) +; COLOR3 is initialized to 70 ($46, red) +; COLOR4 is initialized to 0 ($00, black) + +; $02C9 713 (800) 23 SPARE BYTES +; XL relocatable handler and other variables: +XL_RUNADR = $02C9 ; 713 (XL) LOADER VECTOR +XL_HIUSED = $02CB ; 715 (XL) LOADER VECTOR +XL_ZHIUSE = $02CD ; 717 (XL) LOADER VECTOR +XL_GBYTEA = $02CF ; 719 (XL) LOADER VECTOR +XL_LOADAD = $02D1 ; 721 (XL) LOADER VECTOR +XL_ZLOADA = $02D3 ; 723 (XL) LOADER VECTOR +XL_DSCTLN = $02D5 ; 725 (XL) DISK SECTOR SIZ +XL_ACMISR = $02D7 ; 727 (XL) RESERVED +XL_KRPDER = $02D9 ; 729 (XL) KEY AUTO REPEAT DELAY +XL_KEYREP = $02DA ; 730 (XL) KEY AUTO REPEAT RATE +XL_NOCLIK = $02DB ; 731 (XL) KEY CLICK DISABLE (ctrl-F3 on 1200XL) +XL_HELPFG = $02DC ; 732 (XL) HELP KEY FLAG +XL_DMASAV = $02DD ; 733 (XL) SDMCTL (DMA) SAVE (ctrl-F2 on 1200XL) +XL_PBPNT = $02DE ; 734 (XL) PRINTER BUFFER POINTER (moved from PBPNT on 800) +XL_PBUFSZ = $02DF ; 735 (XL) PRINTER BUFFER SIZE (moved from PBUFSZ on 800) +; note that PTEMP was deleted from the XL OS + +; DOS/FMS variables: +GLBABS = $02E0 ; 736 GLOBAL VARIABLES, 4 SPARE BYTES (if DOS not loaded) + ; If DOS/FMS is loaded: +RUNAD = $02E0 ; 736 (DOS) Run address for binary file (LSB/MSB) +INITAD = $02E2 ; 736 (DOS) Init address for binary file (LSB/MSB) + +; SYSEQU.ASM defines these: +GOADR = RUNAD +INITADR = INITAD + +; OS variables: +RAMSIZ = $02E4 ; 740 PERMANENT START OF ROM POINTER +MEMTOP = $02E5 ; 741 END OF FREE RAM +MEMLO = $02E7 ; 743 LSB, points to bottom of free memory ($0700 if DOS + ; not booted). Not to be confused with BASIC's LOMEM! +; $02E8 ; 744 MSB of MEMLO + +; $02E9 ; 745 (800) SPARE +XL_HNDLOD = $02E9 ; 745 (XL) HANDLER LOADER FLAG + +DVSTAT = $02EA ; 746 DEVICE STATUS BUFFER, 4 BYTES +CBAUDL = $02EE ; 750 CASSETTE BAUD RATE, 2 BYTES +CRSINH = $02F0 ; 752 1 = INHIBIT CURSOR +KEYDEL = $02F1 ; 753 KEY DELAY AND RATE (aka debounce counter) +CH1 = $02F2 ; 754 prior keyboard character code +CHACT = $02F3 ; 755 (shadow for CHACTL) +CHBAS = $02F4 ; 756 CHARACTER SET POINTER (shadow for CHBASE) + +; These next 4 are located elsewhere on the 800 OS: +XL_NEWROW = $02F5 ; 757 (XL) DRAW DESTINATION +XL_NEWCOL = $02F6 ; 758 (XL) DRAW DESTINATION +XL_ROWINC = $02F8 ; 760 (XL) +XL_COLINC = $02F9 ; 761 (XL) +; $02F5 - $02F9 are "spare" on the 800. + +CHAR = $02FA ; 762 most recent character read/written (screen code) +ATACHR = $02FB ; 763 ATASCII CHARACTER FOR CIO +CH = $02FC ; 764 last key pressed (internal scan code) +FILDAT = $02FC ; 764 COLOR FOR SCREEN FILL +DSPFLG = $02FE ; 766 DISPLAY CONTROL CHARACTERS FLAG +SSFLAG = $02FF ; 767 DISPLAY START/STOP FLAFG + +; +; PAGE 3 +; +; +; RESIDENT DISK HANDLER/SIO INTERFACE +; +; The DCB is used for SIO (serial I/O). +DCB = $0300 ; 768 DEVICE CONTROL BLOCK +DDEVIC = $0300 ; 768 device ID ($31-$38 for D1:-D8:) +DUNIT = $0301 ; 769 disk/device unit numder +DCOMND = $0302 ; 770 device command +DSTATS = $0303 ; 771 status code (set by OS) +DBUFLO = $0304 ; 772 data buffer LSB (set by user) +DBUFHI = $0305 ; 773 data buffer MSB (set by user) +DTIMLO = $0306 ; 774 timeout (set by user, units of 60/64 seconds) +DUNUSE = $0307 ; 775 unused +DBYTLO = $0308 ; 776 number of bytes to transfer, LSB +DBYTHI = $0309 ; 777 number of bytes to transfer, MSB +DAUX1 = $030A ; 778 LSB of sector number (for disk) (set by user) +DAUX2 = $030B ; 779 MSB of sector number (for disk) +TIMER1 = $030C ; 780 INITIAL TIMER VALUE +ADDCOR = $030E ; 782 (800) ADDITION CORRECTION +; note that ADDCOR was deleted from the XL OS, and replaced with: +XL_JMPERS = $030E ; 782 (XL) OPTION JUMPERS +CASFLG = $030F ; 783 CASSETTE MODE WHEN SET +TIMER2 = $0310 ; 784 FINAL VALUE, TIMERS 1 & 2 DETERMINE BAUD RATE +TEMP1 = $0312 ; 786 +XL_TEMP2 = $0313 ; 787 (XL) +TEMP2 = $0314 ; 788 (800) +XL_PTIMOT = $0314 ; 788 (XL) PRINTER TIME OUT +TEMP3 = $0315 ; 789 +SAVIO = $0316 ; 790 SAVE SERIAL IN DATA PORT +TIMFLG = $0317 ; 791 TIME OUT FLAG FOR BAUD RATE CORRECTION +STACKP = $0318 ; 792 SIO STACK POINTER SAVE +TSTAT = $0319 ; 793 TEMPORARY STATUS HOLDER +HATABS = $031A ; 794 HANDLER ADDRESS TABLE, 38 BYTES +MAXDEV = $0321 ; 801 MAXIMUM HANDLER ADDRESS INDEX +XL_PUPBT1 = $033D ; 829 (XL) POWER-UP/RESET +XL_PUPBT2 = $033E ; 830 (XL) POWER-UP/RESET +XL_PUPBT3 = $033F ; 831 (XL) POWER-UP/RESET + +; IOCB's, 8 of them, 16 bytes each. +; Set X register to (IOCB number * 16), and use e.g. ICCOM,x +; +IOCB = $0340 ; 832 ; IOCB base address +ICHID = $0340 ; 832 ; Handler ID (set by OS) +ICDNO = $0341 ; 833 ; Device number (set by OS) +ICCOM = $0342 ; 834 ; Command byte (see C_* constants) (set by user) +ICCMD = ICCOM ; ; alternate name for ICCOM, according to Mapping. +ICSTA = $0343 ; 835 ; Status (set by OS) +ICBAL = $0344 ; 836 ; Buffer address, LSB (set by user) +ICBAH = $0345 ; 837 ; Buffer address, MSB (set by user) +ICPTL = $0346 ; 838 ; Put-one-byte address minus one, LSB (set by OS) +ICPTH = $0347 ; 839 ; Put-one-byte address minus one, MSB (set by OS) +ICBLL = $0348 ; 840 ; Buffer length, LSB (set by user) +ICBLH = $0349 ; 841 ; Buffer length, MSB (set by user) +ICAX1 = $034A ; 842 ; AUX1 byte (2nd param in BASIC OPEN) (set by user) +ICAX2 = $034B ; 843 ; AUX2 byte (4rd param in BASIC OPEN) (set by user) +ICAX3 = $034C ; 844 ; AUX3 byte (used by NOTE/POINT) (set by user) +ICAX4 = $034D ; 845 ; AUX4 byte (used by NOTE/POINT) (set by user) +ICAX5 = $034E ; 846 ; AUX5 byte (used by NOTE/POINT) (set by user) +ICAX6 = $034F ; 847 ; Spare aux byte +; OTHER IOCB's, 112 BYTES ($300 + $10 * channel) + +IOCBLEN = *-IOCB ; length of one IOCB (from SYSEQU.ASM) + +; Alternative names for the above. I found these in SYSEQU.ASM, as +; distributed with the disk version of Mac65. +ICBADR = ICBAL +ICPUT = ICPTL +ICBLEN = ICBLL +ICAUX1 = ICAX1 +ICAUX2 = ICAX2 +ICAUX3 = ICAX3 +ICAUX4 = ICAX4 +ICAUX5 = ICAX5 +ICAUX6 = ICAX6 + +PRNBUF = $03C0 ; 960 PRINTER BUFFER, 40 BYTES +; $03E8 ; 1000 (800) 21 SPARE BYTES +XL_SUPERF = $03E8 ; 1000 (XL) SCREEN EDITOR +XL_CKEY = $03E9 ; 1001 (XL) START KEY FLAG +XL_CASSBT = $03EA ; 1002 (XL) CASSETTE BOOT FLAG +XL_CARTCK = $03EB ; 1003 (XL) CARTRIDGE CHECKSUM +XL_ACMVAR = $03ED ; 1005 (XL) RESERVED, 10 BYTES (to $03F7) +XL_BASICF = $03F8 ; 1006 (XL) 0 if ROM-BASIC enabled, 1 if not +XL_MINTLK = $03F9 ; 1017 (XL) RESERVED +XL_GINTLK = $03FA ; 1018 (XL) CARTRIDGE INTERLOCK +XL_CHLINK = $03FB ; 1019 (XL) HANDLER CHAIN, 2 BYTES +CASBUF = $03FD ; 1021 CASSETTE BUFFER, 131 BYTES TO $047F + +; Layout of the cassette buffer after a cassette block is read: + +; Baud correction ($55 $55) bytes are located at offsets 0 and 1 +; Control byte is at offset 2 ($03FF): +; Actual data (128 bytes) runs from offset 3 ($0400) to $047F. +; Each cassette frame has a 1 byte checksum after the 128 data bytes, but +; the checksum is NOT stored anywhere in the cassette buffer! + +; CONTROL BYTE VALUES +; Value Meaning +; 250 ($FA) Partial record follows. The actual number of bytes is stored +; in the last byte of the record (CASBUF+130, or $047F). +; 252 ($FC) Record full; 128 bytes follow. +; 254 ($FE) End of File (EOF) record; followed by 128 zero bytes. + +; Boot tapes normally don't have partial or EOF records, but BASIC +; CLOAD/LOAD/LIST and data file tapes do. + +; Mapping the Atari says the first disk boot sector is read into CASBUF also. + +; +; +; PAGE 4 +; +; +USAREA = $0480 ; 1152 128 SPARE BYTES (but used by BASIC) +; +; SEE APPENDIX C FOR PAGES 4 AND 5 USAGE + +; +; +; +; +; PAGE 5 +; +PAGE5 = $0500 ; 1280 127 FREE BYTES +; $057E 1406 129 FREE BYTES IF FLOATING POINT ROUTINES NOT USED +; +;FLOATING POINT NON-ZERO PAGE RAM, NEEDED ONLY IF FP IS USED +; (20070529 bkw: BASIC constantly uses FP! Also, it uses some of these +; addresses for its own purposes.) +; +LBPR1 = $057E ; 1406 LBUFF PREFIX 1 +LBPR2 = $05FE ; 1534 LBUFF PREFIX 2 +LBUFF = $0580 ; 1408 LINE BUFFER +PLYARG = $05E0 ; 1504 POLYNOMIAL ARGUMENTS +FPSCR = $05E6 ; 1510 PLYARG+FPREC +FPSCR1 = $05EC ; 1516 FPSCR+FPREC +FSCR = $05E6 ; 1510 =FPSCR +FSCR1 = $05EC ; 1516 =FPSCR1 +LBFEND = $05FF ; 1535 END OF LBUFF + +; +; PAGE 6 +; +; +PAGE6 = $0600 ; 1536 256 FREE BYTES + +; +; +; PAGE 7 +; +; +BOOTRG = $0700 ; 1792 PROGRAM AREA +; Boot disks (including DOS) are generally loaded here. Also, BASIC RAM +; (variables and program) starts here, if BASIC is booted without DOS. + +; Page 80 (XL): Self-test (aka diagnostic) ROM is mapped at $5000, +; if enabled with bit 7 of PORTB. Normally only happens if you boot without +; BASIC, cartridge, tape, or disk... or if the OS detects a memory error +; during boot. + +; +; +; UPPER ADDRESSES +; +; +RITCAR = $8000 ;32768 RAM IF NO CARTRIDGE (extends to $9FFF) +LFTCAR = $A000 ;40960 RAM IF NO CARTRIDGE (extends to $BFFF) + +; These 2 are from the Atari System Reference Manual, chapter 12: +CARTA = LFTCAR +CARTB = RITCAR + +CARTLOC = $BFFA ;49146 cartridge run address (from SYSEQU.ASM) + +; Carts were originally 8K only when the 400/800 were first released. +; There were plans to release 16K programs on two cartridges, but this +; never happened (the price of 16K ROMs came down, I guess). 16K cartridges +; go in the left slot, but they actually use the address space for both +; the right and left slots. + +; Mapping the Atari has this to say about cartridges: +;; Byte Purpose +;; Left (A) Right(B) +;; 49146 ($BFFA) 40954 ($9FFA) Cartridge start address (low byte) +;; +;; 49147 ($BFFB) 40955 ($9FFB) Cartridge start address (high byte) +;; +;; 49148 ($BFFC) 40956 ($9FFC) Reads zero if a cartridge is +;; inserted, non-zero when no cartridge is present. This information +;; is passed down to the page zero RAM: if the A cartridge is plugged +;; in, then location 6 will read one; if the B cartridge is plugged in, +;; then location 7 will read one; otherwise they will read zero. +;; +;; 49149 ($BFFD) 40957 ($9FFD) Option byte. If BIT 0 equals one, +;; then boot the disk (else there is no disk boot). If BIT 2 equals one, +;; then initialize and start the cartridge (else initialize but do not +;; start). If BIT 7 equals one, then the cartridge is a diagnostic +;; cartridge which will take control, but not initialize the OS (else +;; non-diagnostic cartridge). Diagnostic cartridges were used by +;; Atari in the development of the system and are not available to the +;; public. +;; +;; 49150 ($BFFE) 40958 ($9FFE) Cartridge initialization address +;; low byte. +;; +;; 49151 ($BFFF) 40959 ($9FFF) Cartridge initialization address +;; high byte. This is the address to which the OS will jump during all +;; powerup and RESETs. +;; +;; The OS makes temporary use of locations 36876 to 36896 ($900C to +;; $9020) to set up vectors for the interrupt handler. See the OS +;; listings pages 31 and 81. This code was only used in the +;; development system used to design the Atari. + + +; Page 192 + +C0PAGE = $C000 ;49152 (800) EMPTY, 4K BYTES + ; 20070529 bkw: unmapped address space. + ; Mapping the Atari erroneously lists this as "unused ROM". + ; There are upgrades to the 800 to give 4K of RAM here + ; (for a total of 52K of RAM), or ROM (Omnimon?). + ; Also, there is RAM here if you boot the Translator + ; disk on an XL. + +; (XL) $C000 also contains info about the ROM revision. From Mapping: + +;Bytes 49152-49163 ($C000-$C00B) are used to identify the computer +;and the ROM in the $C000-$DFFF block: +; +;Byte Use +;49152-3/C000-1 Checksum (LSB/MSB) of all the bytes +; in ROM except the checksum bytes +; themselves. +;49154/C002 Revision date, stored in the form +; DDMMYY. This is DD, day, usually $10. +;49155/C003 Revision date, month; usually $05. +;49156/C004 Revision date, year; usually $83. +;49157/C005 Reserved option byte; reads zero for +; the 1200, 800XL, and 130XE. +;49158/C006 Part number in the form AANNNNNN; +; AA is an ASCII character and +; NNNNNN is a four-bit BCD digit. This is +; byte A1. +;49159-62/C007-A Part number, bytes A2, N1-N6 (each +; byte has two N values of four bits +; each). +;49163/C00B Revision number. Mapping author's 800XL and 130XE say 2. + +;C0PAGE = $C000 ;49152 (XL) OS ROM, mostly interrupt handlers +; $C800 51200 (XL) START OF OS ROM +CHORG2 = $CC00 ;52224 (XL) INTERNATIONAL CHARACTER SET + + + +; +; +; HARDWARE REGISTERS +; +; +; SEE REGISTER LIST FOR MORE INFORMATION +; +; + +; GTIA +GTIA = $D000 +HPOSP0 = $D000 ;53248 (W) ; P/M positions (no shadows) +HPOSP1 = $D001 ;53249 (W) +HPOSP2 = $D002 ;53250 (W) +HPOSP3 = $D003 ;53251 (W) +HPOSM0 = $D004 ;53252 (W) +HPOSM1 = $D005 ;53253 (W) +HPOSM2 = $D006 ;53254 (W) +HPOSM3 = $D007 ;53255 (W) +SIZEP0 = $D008 ;53256 (W) ; P/M size regs (no shadows) +SIZEP1 = $D009 ;53257 (W) +SIZEP2 = $D00A ;53258 (W) +SIZEP3 = $D00B ;53259 (W) +SIZEM = $D00C ;53260 (W) +M0PF = $D000 ;53248 (R) ; collision regs (no shadows) +M1PF = $D001 ;53249 (R) +M2PF = $D002 ;53250 (R) +M3PF = $D003 ;53251 (R) +P0PF = $D004 ;53252 (R) +P1PF = $D005 ;53253 (R) +P2PF = $D006 ;53254 (R) +P3PF = $D007 ;53255 (R) +M0PL = $D008 ;53256 (R) +M1PL = $D009 ;53257 (R) +M2PL = $D00A ;53258 (R) +M3PL = $D00B ;53259 (R) +P0PL = $D00C ;53260 (R) +P1PL = $D00D ;53261 (R) +P2PL = $D00E ;53262 (R) +P3PL = $D00F ;53263 (R) +GRAFP0 = $D00D ;53261 (W) ; direct (non-DMA) P/M graphics regs (no shadows) +GRAFP1 = $D00E ;53262 (W) +GRAFP2 = $D00F ;53263 (W) +GRAFP3 = $D010 ;53264 (W) +GRAFM = $D011 ;53265 (W) +TRIG0 = $D010 ;53264 (R) ; Joystick triggers (shadows @ STRIG0-3) +TRIG1 = $D011 ;53265 (R) +TRIG2 = $D012 ;53266 (R) +TRIG3 = $D013 ;53267 (R) +PAL = $D014 ;53268 (R) ; PAL/NTSC detect (no shadow) + ; PAL supposedly moved to XL_PALNTS on XL; what was it + ; replaced with? +COLPM0 = $D012 ;53266 (W) ; P/M colors (shadows @ PCOLR0-3) +COLPM1 = $D013 ;53267 (W) +COLPM2 = $D014 ;53268 (W) +COLPM3 = $D015 ;53269 (W) +COLPF0 = $D016 ;53270 (W) ; Playfield colors (shadows @ COLOR0-3) +COLPF1 = $D017 ;53271 (W) +COLPF2 = $D018 ;53272 (W) +COLPF3 = $D019 ;53273 (W) +COLBK = $D01A ;53274 (W) ; Background color (shadow @ COLOR4) +PRIOR = $D01B ;53275 (W) ; GTIA priority (shadow @ GPRIOR) +GTIAR = $D01B ;53275 (R?) +VDELAY = $D01C ;53276 (W) +GRACTL = $D01D ;53277 (W) +HITCLR = $D01E ;53278 (W), latch +CONSOL = $D01F ;53279 (W=keyclick spkr, R=console keys) + +; $D020 - $D0FF are mirrors of GTIA address space +; $D100 - $D1FF are supposed to be unused (unmapped) on the 800 +; On the XL, $D100 - $D1FF is switched to device memory during PBI I/O + +; POKEY +POKEY = $D200 +; no shadows for AUDC/AUDF +AUDF1 = $D200 ;53760 (W) ; Audio frequency 1 +AUDC1 = $D201 ;53761 (W) ; Audio control 1 (distortion/volume) +AUDF2 = $D202 ;53762 (W) +AUDC2 = $D203 ;53763 (W) +AUDF3 = $D204 ;53764 (W) +AUDC3 = $D205 ;53765 (W) +AUDF4 = $D206 ;53766 (W) +AUDC4 = $D207 ;53767 (W) + +; POT0-7 shadows at PADDL0-7 +POT0 = $D200 ;53760 (R) ; Paddle positions +POT1 = $D201 ;53761 (R) +POT2 = $D202 ;53762 (R) +POT3 = $D203 ;53763 (R) +POT4 = $D204 ;53764 (R) ; pots 3-7 don't exist on XL/XE +POT5 = $D205 ;53765 (R) +POT6 = $D206 ;53766 (R) +POT7 = $D207 ;53767 (R) + +AUDCTL = $D208 ;53768 (W) ; Audio control (no shadow) +ALLPOT = $D208 ;53768 (R) (no shadow) +STIMER = $D209 ;53769 (W) (no shadow) +KBCODE = $D209 ;53769 (R) (shadow @ CH) +SKREST = $D20A ;53770 (W) (latch) +RANDOM = $D20A ;53770 (R) (no shadow) +POTGO = $D20B ;53771 (W) (latch) +; $D20C (53772) is unused +SEROUT = $D20D ;53773 (W) (no shadow) +SERIN = $D20D ;53773 (R) (no shadow) +IRQEN = $D20E ;53774 (W) (shadow @ POKMSK) +IRQST = $D20E ;53774 (R) +SKCTL = $D20F ;53775 (W) (shadow @ SSKCTL) +SKSTAT = $D20F ;53775 (R) + +; $D210 - $D2FF are mirrors of POKEY address space. The "stereo POKEY" +; modification adds a second POKEY chip, usually addressed at $D210. + +; PIA +; No shadow regs for PIA regs +PIA = $D300 +PORTA = $D300 ;54016 +PORTB = $D301 ;54017 +PACTL = $D302 ;54018 +PBCTL = $D303 ;54019 + +; $D304 - $D3FF are mirrors of PIA address space + +; ANTIC +ANTIC = $D400 +DMACTL = $D400 ;54272 (W) (shadow @ SDMCTL) +CHACTL = $D401 ;54273 (W) (shadow @ CHACT) +DLISTL = $D402 ;54274 (W) (shadow @ SDLSTL) +DLISTH = $D403 ;54275 (W) (shadow @ SDLSTH) +HSCROL = $D404 ;54276 (W) (no shadow) +VSCROL = $D405 ;54277 (W) (no shadow) +; $D406 (54278) is unused +PMBASE = $D407 ;54279 (W) (no shadow) +; $D408 (54280) is unused +CHBASE = $D409 ;54281 (W) (shadow @ CHBAS) +WSYNC = $D40A ;54282 (W), latch (data written doesn't matter) +VCOUNT = $D40B ;54283 (R) (no shadow) +PENH = $D40C ;54284 (R) (shadow @ LPENH) +PENV = $D40D ;54285 (R) (shadow @ LPENV) +NMIEN = $D40E ;54286 (W) (no shadow) +NMIRES = $D40F ;54287 (W), latch? +NMIST = $D40F ;54287 (R) (no shadow) + +; $D410 - $D4FF are mirrors of ANTIC address space + +CCNTL = $D500 ;54528 Cartridge control (sometimes used for bankswitching) +; $D500 - $D5FF is supposed to be all be mapped to CCNTL + +; $D600 - $D7FF is unmapped? used by PBI on XL? seems to read all $FF + +; +; FLOATING POINT MATH ROUTINES +; +; From Mapping: +; These entry points are the same on 400/800 and XL OS, though the +; routines themselves are different (bugfixed/optimized for XL) +; Also, on the XL, the $D800 area is bankswitched to PBI device ROM, +; during PBI I/O. Not sure if all of $D800 - $DFFF is switched out +; or just part of it. +AFP = $D800 ;55296 ASCII to Floating Point (FP) conversion. +FASC = $D8E6 ;55526 FP value to ASCII conversion. +IFP = $D9AA ;55722 Integer to FP conversion +FPI = $D9D2 ;55762 FP to Integer conversion +ZFR0 = $DA44 ;55876 Clear FR0 (set all bytes to 0) +ZF1 = $DA46 ;55878 Clear FR1 (set all bytes to 0) (aka AF1 (De Re)) +FSUB = $DA60 ;55904 FP subtract: FR0 = FR0 - FR1 +FADD = $DA66 ;55910 FP add: FR0 = FR0 + FR1 +FMUL = $DADB ;56027 FP multiply: FR0 = FR0 * FR1 +FDIV = $DB28 ;56104 FP divide: FR0 = FR0 / FR1 +PLYEVL = $DD40 ;56640 FP polynomial evaluation +FLD0R = $DD89 ;56713 Load FP number into FR0 from 6502 X/Y registers +FLD0P = $DD8D ;56717 Load FP number into FR0 from FLPTR +FLD1R = $DD98 ;56728 Load FP number into FR1 from 6502 X/Y registers +FLD1P = $DD9C ;56732 Load FP number into FR1 from FLPTR +FST0R = $DDA7 ;56743 Store FP number into 6502 X/Y regs from FR0 +FST0P = $DDAB ;56747 Store FP number from FR0, using FLPTR +FMOVE = $DDB6 ;56758 Move FP number from FR0 into FR1 (FR1 = FR0) +EXP = $DDC0 ;56768 FP base e exponentiation +EXP10 = $DDCC ;56780 FP base 10 exponentiation +LOG = $DECD ;57037 FP natural logarithm +LOG10 = $DED1 ;57041 FP base 10 logarithm + +; +; +; OPERATING SYSTEM +; +; +; MODULE ORIGIN TABLE +; +CHORG = $E000 ;57344 CHARACTER SET, 1K +VECTBL = $E400 ;58368 VECTOR TABLE +VCTABL = $E480 ;58496 RAM VECTOR INITIAL VALUE TABLE +CIOORG = $E4A6 ;58534 CIO HANDLER +INTORG = $E6D5 ;59093 INTERRUPT HANDLER +SIOORG = $E944 ;59716 SIO DRIVER +DSKORT = $EDEA ;60906 DISK HANDLER +PRNORG = $EE78 ;61048 PRINTER HANDLER +CASORG = $EE78 ;61048 CASSETTE HANDLER +MONORG = $F0E3 ;61667 MONITOR/POWER UP MODULE +KBDORG = $F3E4 ;62436 KEYBOARD/DISPLAY HANDLER +; +; +; VECTOR TABLE, CONTAINS ADDRESSES OF CIO ROUTINES IN THE +; FOLLOWING ORDER. THE ADDRESSES IN THE TABLE ARE TRUE ADDRESSES-1 +; +; ADDRESS + 0 OPEN +; + 2 CLOSE +; + 4 GET +; + 6 PUT +; + 8 STATUS +; + A SPECIAL +; + C JMP TO INITIALIZATION +; + F NOT USED +; +; + +; 20070529 bkw: why are they address minus one? because they are called +; via RTS: a JSR actually pushes the return address minus one, and RTS +; increments the address on the stack after popping it. The Atari OS +; "pretends" to have done a JSR by pushing the address-1 on the stack, +; then executes RTS, which "returns" to the correct address. + +EDITRV = $E400 ;58368 EDITOR +SCRENV = $E410 ;58384 SCREEN +KEYBDV = $E420 ;58400 KEYBOARD +PRINTV = $E430 ;58416 PRINTER +CASETV = $E440 ;58432 CASSETTE +; +; ROM VECTORS +; +; 20070529 bkw: These consist of a JMP xxxx instruction in the ROM. +DSKINV = $E453 ;58451 +CIOV = $E456 ;58454 ; Main CIO entry point! +SIOV = $E459 ;58457 ; Main SIO entry point! +SETVBV = $E45C ;58460 +SYSVBV = $E45F ;58463 +VBIVAL = $E460 ;58464 ADR AT VVBLKI (operand of JMP @ $E45F) +XITVBV = $E462 ;58466 EXIT VBI +VBIXVL = $E463 ;58467 ADR AT VVBLKD (operand of JMP @ $E462) +SIOINV = $E465 ;58469 +SENDEV = $E468 ;58472 +INTINV = $E46B ;58475 +CIOINV = $E46E ;58478 +BLKBDV = $E471 ;58481 MEMO PAD MODE (self-test in XL) +WARMSV = $E474 ;58484 ; warmstart (RESET key jumps here) +COLDSV = $E477 ;58487 ; coldstart (reboot) the Atari +RBLOKV = $E47A ;58490 +CSOPIV = $E47D ;58493 + +; SYSEQU.ASM defines this: +CIO = CIOV + +; XL-only entry points: +XL_SELFSV = BLKBDV ; self-test (same entry point as 800 memo pad) +XL_SELFTST = BLKBDV ; alt. name (Mapping) +XL_PUPDIV = $E480 ;58496 (XL) Power-up ATARI logo (1200XL only), or self-test +XL_SLFTSV = $E483 ;58499 (XL) Self-test vector (points to $5000) +XL_PENTV = $E486 ;58502 (XL) Entry to the handler uploaded from peripheral + ; or disk (is this for the PBI?) +XL_PHUNLV = $E489 ;58505 (XL) Entry to uploaded handler unlink (PBI?) +XL_PHINIV = $E48C ;58508 (XL) Entry to uploaded handler init (PBI?) +XL_GPDVV = $E48F ;58511 (XL) General-purpose parallel device handler + ; (copy to HATABS to use) + +;;;;; Here endeth the list of official mnemonics + +; Mapping has this to say about the XL ROMs: +;Byte Use +;65518/FFEE Revision date D1 and D2 (four-bit BCD) +;65519/FFEF Revision date M1 and M2 +;65520/FFF0 Revision date Y1 and Y2 +;65521/FFF1 Option byte; should read 1 for the +; 1200XL (Mapping author's 800XL reads 2) +;65522-26/FFF2-6 Part number in the form AANNNNNN +;65527/FFF7 Revision number (again, mine reads 2) +;65528-9/FFF8-9 Checksum, bytes (LSB/MSB) +; There don't seem to be any known mnemonics for the above... + +; 20061120 bkw: display list stuff. These are not official Atari mnemonics, +; but they *are* somewhat based on the "Checkers Demo" by Carol Shaw, +; in the Atari Hardware Manual (she didn't define all these, and she didn't +; use the "DL_" prefix, probably because her assembler was limited to +; 6-character labels and/or didn't support the underscore). + +; blank lines, 1-8 scanlines high +DL_BLANK1 = $00 +DL_BLANK2 = $10 +DL_BLANK3 = $20 +DL_BLANK4 = $30 +DL_BLANK5 = $40 +DL_BLANK6 = $50 +DL_BLANK7 = $60 +DL_BLANK8 = $70 + +; modifier bits.. +DL_VSCROLL = $10 +DL_HSCROLL = $20 +DL_LMS = $40 +DL_DLI = $80 + +; graphics modes (these are the BASIC modes) +; If you're more familiar with the ANTIC modes, nobody's forcing you +; to use these :) +DL_GR0 = $02 +DL_GR1 = $06 +DL_GR2 = $07 +DL_GR3 = $08 +DL_GR4 = $09 +DL_GR5 = $0A +DL_GR6 = $0B +DL_GR7 = $0D +DL_GR8 = $0F +DL_GR12 = $04 ; GR. 12-15 only supported by GRAPHICS command on XL/XE, +DL_GR13 = $05 ; but they exist on all ANTIC revisions +DL_GR14 = $0C +DL_GR15 = $0E ; AKA "graphics 7.5" +; No GRAPHICS mode for ANTIC $03 (true descender) mode + +; jump instructions +DL_JMP = $01 ; jump without vertical blank (used to skip over 1K boundary) +DL_JVB = $41 ; jump & wait for VBLANK (end of display list) + +; How to use the above: here's a sample display list for GR.0, with a DLI +; on screen line 10. + +; dlist: +; ; 4*8 = 32 blank lines at start of display +; byte DL_BLANK8, DL_BLANK8, DL_BLANK8, DL_BLANK8 +; +; byte DL_GR0 | DL_LMS ; display GR.0 line, and load screen memory address.. +; word screen_ram ; ...from our screen memory (declared elsewhere) +; +; ; 8 more GR.0 lines +; byte DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0 +; +; byte DL_GR0 | DL_DLI ; another GR.0 line, with the DLI bit enabled +; +; ; lines 11-24 (14 more GR.0 bytes) +; byte DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0 +; byte DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0, DL_GR0 +; +; ; that's 24 lines, so finish with a VBLANK +; byte DL_JVB ; jump (and wait), to... +; word dlist ; ...the beginning. diff --git a/fenders.1 b/fenders.1 new file mode 100644 index 0000000..aa6d5fc --- /dev/null +++ b/fenders.1 @@ -0,0 +1,282 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "FENDERS" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +fenders \- Install Fenders 3-sector loader in boot sectors of an ATR image +.\" RST source for fenders(1) man page. Convert with: +. +.\" rst2man.py fenders.rst > fenders.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fIfenders\fP [\fI\-hrcsiv\fP] [\-t \fItitle\fP] \fIinfile.atr\fP [\fIoutfile.atr\fP] +.SH DESCRIPTION +.sp +\fBfenders\fP replaces the boot sectors of an ATR image with a menu\-driven +"game dos" binary loader. +.sp +When the disk is booted, a disk directory menu is displayed on the +screen. To load a binary (XEX/COM/BIN/etc) file, press the letter +shown for that file. Other file types may not be loaded (and will cause +the Atari to crash). +.sp +The installed bootloader overwrites sectors 1\-3 of the image +(and sector 720, for double\-density images). This is the same code +installed by the Atari\-based \fBFenders 3\-sector\fP loader installer +utility. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-h +Print this help message. +.TP +.B \-r +DON\(aqT reboot (coldstart) the Atari if Reset is pressed. The default is to coldstart. +.TP +.B \-c +Rotate colors during load. May cause problems with some games. +.TP +.B \-s +Screen off after load. May cause problems with some games, but +may fix graphics corruption for other games. YMMV. +.TP +.B \-i +In\-place update. The input file is renamed to end in \fB~\fP (tilde), +and the output is written to the original filename. May not be +used when reading from standard input. +.TP +.B \-v +Set inverse video bit in title. This causes the title text to +appear in red and/or blue on the Atari, instead of the default +orange and green colors. +.TP +.B \-t +Set the menu title. May up to 20 characters; default is \fIatari +arcade\fP\&. Will be truncated to 20 characters if a longer title is +given. See MENU TITLE, below. Note that you\(aqll have to quote the +title if it contains spaces or other characters that have meaning to your shell. +.UNINDENT +.INDENT 0.0 +.TP +.B infile +The ATR image to read from. It must be either single\-density +and at least 368 sectors long, or double\-density and at least +720 sectors long. You may use \fB\-\fP for \fIinfile\fP to read from +standard input (unless \fB\-i\fP is used). +.TP +.B outfile +The ATR image to create, which will contain all the files +from the input image, plus the Fenders boot loader. You may +omit \fIoutfile\fP or use \fB\-\fP to write to standard output. \fIoutfile\fP is +ignored when the \fB\-i\fP option is used. +.UNINDENT +.sp +If both \fIinfile\fP and \fIoutfile\fP are omitted, the default is to read from +standard input and write to standard output. +.SH NOTES +.sp +\fBfenders\fP will abort, if asked to write to standard output when standard +output is a terminal. This is to avoid spewing binary garbage to your +terminal. +.sp +The \fB\-r\fP, \fB\-c\fP, \fB\-s\fP, and \fB\-t\fP options actually modify the \fBfenders\fP 6502 object +code before writing it to the ATR image. +.sp +The Atari \fBfenders\fP installer\(aqs default is to not coldstart, which +tends to cause the Atari to lock up when Reset is pressed. The +author believes that rebooting the Atari is more useful than locking +it up, so \fBfenders\fP causes the coldstart by default. +.sp +The Atari \fBfenders\fP installer contains a bug: the meanings of the "Rotate +color" and "Screen off after load" options are reversed. \fBfenders\fP does +not duplicate this bug. +.sp +The disk density doesn\(aqt have to be specified, because \fBfenders\fP reads it +from the ATR header. The loader\(aqs 6502 object code is different, for +single\- and double\-density disks. +.sp +The double\-density version of the loader isn\(aqt actually a 3\-sector +loader. The first 384 bytes of its code are stored in the 3 boot sectors, and the last 256 bytes of code are actually read from sector 720. +When installing the boot loader on a double\-density disk, sector 720 is +\fBoverwritten\fP\&. Any data that may have been there is lost. On most disks, +sector 720 is either unusable by DOS, or not used unless the disk is +completely full, so this is less of a problem than you might think. +.sp +\fBfenders\fP and the \fBfenders\fP boot\-loader code will work with DOS +2.5 "enhanced density" formatted floppies, but only partially: files +that use sectors above 720 will not appear in the menu (these are the +same files that DOS 2.5 lists with <> around the filename). +.sp +\fBfenders\fP only works on Atari DOS 2.x and compatible (MyDOS, DOS XL, et +al) single\-sided disk images, either single\-density (720 sectors, 90K), +double\-density (720 sectors, 180K), or "1050 enhanced" density (1040 +sectors, 130K, although 1050 enhanced density images must be in DOS 2.5 +format, \fInot\fP MyDOS, and see \fBLIMITATIONS\fP below). Other non\-standard DOS +formats such as SpartaDOS or Atari DOS 3.0 and 4.0 are not supported. +Atari DOS 1.0 may or may not work (untested). +.sp +The \fBfenders\fP boot loader source code distributed with \fBfenders\fP +is not the original source code (which has never been +released). The author of \fBfenders\fP spent a couple of days with a +disassembly of the object code and reverse engineered (labelled, +commented) it so that humans can read it, provided they are humans who +speak 6502 assembly. The \fIfenders.dasm\fP and \fIfendersdbl.dasm\fP files +can be assembled with the DASM cross assembler. The resulting object +code is identical with the original \fBfenders\fP object code (which +the author read from a disk image in the first place). +.sp +Contrary to what you may have read, the Fenders boot loader code +doesn\(aqt have to be rewritten to a disk if you add/delete files. It +reads the disk directory, and can cope with changes just fine. Even a +heavily fragmented filesystem won\(aqt cause any problems. +.SH MENU TITLE +.sp +The menu title (set with \fB\-t\fP) is stored in sector 3 for a +single\-density disk or sector 720 of a double\-density disk, in Atari +internal screen codes (which are not the same as either ASCII or +ATASCII). fenders converts the title into internal codes, so the user +doesn\(aqt have to worry about this. +.sp +The menu title is displayed in "GRAPHICS 2" mode, which can only +display 64 different characters, including uppercase, numbers, and +(most) punctuation, but NOT including lowercase or inverse video. +.sp +However, lowercase letters are displayed as different\-colored +uppercase letters, and inverse characters are also displayed with +different colors. The boot loader uses the default Atari +colors, as set by the Atari OS. +.sp +The title is limited to 20 characters because that\(aqs the width of a +GRAPHICS 2 line of text on the Atari. +.sp +The character set used by GRAPHICS 2 consists of: +.INDENT 0.0 +.IP \(bu 2 +The space character. +.IP \(bu 2 +The letters \fBA\-Z\fP\&. +.IP \(bu 2 +The numbers \fB0\-9\fP\&. +.IP \(bu 2 +Punctuation: \fB!\fP \fB"\fP \fB#\fP \fB$\fP \fB%\fP \fB&\fP \fB\(aq\fP \fB(\fP \fB)\fP \fB*\fP \fB+\fP \fB,\fP \fB\-\fP \fB\&.\fP \fB/\fP \fB:\fP \fB;\fP \fB<\fP \fB=\fP \fB>\fP \fB?\fP \fB@\fP \fB[\fP \fB\e\fP \fB]\fP \fB^\fP \fB_\fP +.UNINDENT +.sp +Uppercase letters, numbers, and punctuation are displayed in orange (or +blue, if \fB\-v\fP is used). +.sp +Lowercase letters are displayed in green (or red, with \fB\-v\fP). +.sp +The characters \(ga { | } ~ are displayed as green (or red) versions of +@ [ ] ^, respectively. +.sp +Currently, it\(aqs not possible to mix normal (orange/green) characters +with inverse (blue/red) with fenders, unless your terminal provides a +way for you to enter ASCII characters with their high bits set. Some +versions of xterm(1x) allow this with the Alt or Meta key. +.SH LIMITATIONS +.sp +\fBfenders\fP should warn if the disk image contains more than 20 files, +since they won\(aqt all be displayed in the menu. +.sp +Double\-density images less than 720 sectors long are not handled, +although they could be with a little more work. +.sp +For double\-density disks, the VTOC and sector link bytes should +be checked to see if sector 720 is in use, rather than just blindly +overwriting it. On a single\-density DOS 2.0S disk, sector 720 is +marked "in use" in the VTOC when the disk is formatted, but will never +be used for file storage. +.sp +No checking for non\-standard formats (SpartaDOS, DOS 3, MyDOS with +subdirectories, dedicated bootdisks, etc) is done. +.sp +If an "enhanced" 1050 density disk image has the bootloader installed, +it won\(aqt display the files using sectors above 720 (the ones which +would appear as \fI<filename.ext>\fP in the DOS 2.5 directory). +.sp +Actually, the above limitations are a direct result of the fact that +\fBfenders\fP deals with the disk image at the "sector" level, and contains +no code that understands the files, directory, or VTOC on the image. +The original Atari\-based Fenders installer shares the same limitations, +so the author doesn\(aqt really consider them to be bugs, although they +may be addressed in a future version of \fBfenders\fP\&. +.sp +Note that the bootloader only displays up to 20 files on the screen. +This shouldn\(aqt be a real problem (how many games can you fit on a 180K +floppy?), but no warning or error is given if there are too many files. +.sp +The bootloader only supports standard SIO disk speed (19200bps). The +only way to make it support high\-speed SIO is to use the APE Warp OS +(or some other high\-speed patched OS) on the Atari. +.SH BUGS +.sp +There should be an option to delete DOS.SYS and DUP.SYS from the image, +but there isn\(aqt. The original Fenders installer has this option. +.sp +When used with an image whose size according to the ATR header +doesn\(aqt match the actual image file size, \fBfenders\fP may produce +a broken or zero\-length output file. If in doubt, use \fBatrcheck\fP to +validate the image before use. +.sp +There should be a way to install an arbitrary 3\-sector (384\-byte) +binary file in the boot sectors of an image. This can be done with +\fBdd\fP, of course, but its syntax is difficult to remember, and typos are +prone to wipe out the file you\(aqre working with. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/fenders.bin b/fenders.bin Binary files differnew file mode 100644 index 0000000..ee8146c --- /dev/null +++ b/fenders.bin diff --git a/fenders.c b/fenders.c new file mode 100644 index 0000000..55ad709 --- /dev/null +++ b/fenders.c @@ -0,0 +1,418 @@ +#include <stdio.h> +#include <unistd.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> +#include <ctype.h> + +#include "fenders_bin.h" +#include "fenders_offsets.h" +#include "fendersdbl_bin.h" +#include "fendersdbl_offsets.h" + +extern char *optarg; +extern int optind, opterr, optopt; + +#ifndef VERSION +#define VERSION "???" +#endif + +#define SELF "fenders" +#define BANNER SELF " v" VERSION " by B. Watson (WTFPL)\n" +#define DEFAULT_TITLE "atari arcade" +#define OPTIONS "hrscit:v" + +char *usage = + BANNER + "Install Fenders 3-sector loader in boot sectors of an ATR image\n" + "Usage: " SELF " -[hrcsiv] [-t title] infile.atr [outfile.atr]\n" + " -h Print this help message\n" + " -r DON'T reboot (coldstart) the Atari if Reset is pressed\n" + " -c Rotate colors during load\n" + " -s Screen off after load\n" + " -i In-place update (original renamed to end in ~)\n" + " -v Set inverse video bit in title (blue/red text)\n" + " -t title Set title (up to 20 chars, default: '" DEFAULT_TITLE "')\n"; + +typedef enum { SD, DD } density; + +void set_title(char *title, density dens, int inverse) { + int i; + int offset; + unsigned char *bin; + int len = strlen(title); + + if(dens == SD) { + offset = OFFSET_TITLE; + bin = fenders_bin; + } else { + offset = OFFSET_TITLE_DD; + bin = fendersdbl_bin; + } + + /* zero out the title area first (zeroes are Atari spaces BTW) */ + memset(&bin[offset], 0, 20); + + if(len > 20) { + len = 20; + fprintf(stderr, SELF ": Truncating title to 20 characters\n"); + } else if(!len) { + return; + } + + /* convert ASCII to Atari screen codes (not the same as ATASCII) + charset in GR.2 is space plus: + !"#$%'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ + + Punctuation comes out orange (blue with -v) + Digits and uppercase letters are orange/blue + Lowercase letters are green/red + ` = green/red @ + { = green/red [ + | = green/red ^ + } = green/red ] + ~ = green/red ^ + */ + for(i=0; i<len; i++) { + char c = title[i]; + + if(c >= 32 && c <= 95) + c -= 32; + + if(inverse) + c |= 128; + + title[i] = c; + } + + /* Center title in 20-byte title area */ + memcpy(&bin[offset + 10 - len / 2], title, len); + + /* + fprintf(stderr, SELF ": Set title to \""); + for(i=0; i<20; i++) { + char c = bin[offset + i]; + if(!c) c = ' '; + if(!isprint(c)) c = '.'; + putc(c, stderr); + } + + putc('"', stderr); + putc('\n', stderr); + */ +} + +/* for set_coldstart(), set_rot_color(), set_screen_off(), + see fenders.dasm (and fendersdbl.dasm) to understand what's going on. + Search for the OFFSET_* strings. */ + +void set_coldstart(density dens) { + if(dens == SD) { + fenders_bin[OFFSET_COLDST_1] = 1; /* operand for LDY # (replaces 0) */ + fenders_bin[OFFSET_COLDST_2] = 0xea; /* NOP (replaces INY) */ + } else { + fendersdbl_bin[OFFSET_COLDST_1_DD] = 1; + fendersdbl_bin[OFFSET_COLDST_2_DD] = 0xea; + } +} + +void set_rot_color(density dens) { + if(dens == SD) + fenders_bin[OFFSET_ROTCOLOR] = 0x8d; /* STA abs (replace LDA abs) */ + else + fendersdbl_bin[OFFSET_ROTCOLOR_DD] = 0x8d; +} + +void set_screen_off(density dens) { + if(dens == SD) + fenders_bin[OFFSET_SCREENOFF] = 0x8d; /* STA abs (replace LDA abs) */ + else + fendersdbl_bin[OFFSET_SCREENOFF_DD] = 0x8d; +} + +int main(int argc, char **argv) { + int coldstart = 1, rot_color = 0, screen_off = 0; + int c, res, size, inverse = 0; + char title[21]; + int in_place = 0; + char rename_to[4096]; + unsigned char buf[384], *bin; + char *infile = "-", *outfile = "-"; + FILE *in = stdin, *out = stdout; + density dens; + + /* initialize title (may be changed by -t option) */ + strcpy(title, DEFAULT_TITLE); + + /* parse options */ + while( (c = getopt(argc, argv, OPTIONS)) != -1) { + switch(c) { + case 'h': + printf(usage); + exit(0); + break; + + case 'r': + coldstart = 0; + break; + + case 'c': + rot_color = 1; + break; + + case 's': + screen_off = 1; + break; + + case 't': + strcpy(title, optarg); + break; + + case 'i': + in_place = 1; + break; + + case 'v': + inverse = 1; + break; + + default: + fprintf(stderr, usage); + exit(1); + break; + } + } + + /* get input filename if present */ + if(optind < argc) + infile = argv[optind++]; + + /* get output filename if present */ + if(!in_place && optind < argc) + outfile = argv[optind++]; + + if(optind < argc) { + fprintf(stderr, SELF + ": Ignoring extra junk '%s ...' on command line.\n", + argv[optind]); + } + + if(in_place) { + /* rename infile to infile~, set outfile to old infile */ + int len = strlen(infile); + + if(strcmp(infile, "-") == 0) { + fprintf(stderr, SELF + ": Can't use in-place mode with standard input.\n"); + exit(1); + } + + strcpy(rename_to, infile); + rename_to[len] = '~'; + rename_to[len + 1] = '\0'; + + fprintf(stderr, SELF ": Backing up %s to %s\n", infile, rename_to); + if(link(infile, rename_to)) { + perror("link()"); + exit(1); + } + + if(unlink(infile)) { + perror("unlink()"); + exit(1); + } + + outfile = infile; + infile = rename_to; + } + + /* open input and output files, if not stdin/stdout */ + if(strcmp(infile, "-") != 0) { + in = fopen(infile, "rb"); + if(!in) { + perror(infile); + exit(1); + } + } + + /* read ATR header */ + res = fread(buf, 1, 16, in); + if(res < 16) { + perror(infile); + exit(1); + } + + /* make sure it's an ATR image */ + if( !(buf[0] == 0x96 && buf[1] == 0x02) ) { + fprintf(stderr, SELF + ": %s not an ATR file (no NICKATARI signature)!\n" + "If this is an XFD file, try xfd2atr\n", + infile); + exit(2); + } + + /* get sector size. The single- and double-density versions of + the loader are totally different, so pick the one we need. */ + if( (buf[4] == 0x80 && buf[5] == 0x00) ) { + dens = SD; + bin = fenders_bin; + } else if( (buf[4] == 0x00 && buf[5] == 0x01) ) { + dens = DD; + bin = fendersdbl_bin; + } else { + fprintf(stderr, SELF ": ATR image must have 128 or 256 byte sectors\n"); + exit(2); + } + + /* modify the loader according to the user's options */ + set_title(title, dens, inverse); + if(coldstart) set_coldstart(dens); + if(rot_color) set_rot_color(dens); + if(screen_off) set_screen_off(dens); + + /* size of ATR image in bytes (minus the header). We don't support + DD images less than 720 sectors. */ + size = (buf[2] + (buf[3] << 8) + (buf[6] << 16)) * 16; + if(dens == SD && size < (128 * 369)) { + fprintf(stderr, SELF + ": ATR is single density < 369 sectors, not supported\n" + "Use atrsize to grow the image.\n"); + exit(2); + } else if(dens == SD && size > (128 * 720)) { + fprintf(stderr, SELF + ": ATR is single density > 720 sectors; " + "some files may not appear in menu.\n"); + } else if(dens == DD && size < (128 * 3 + 256 * 717)) { + fprintf(stderr, SELF + ": ATR is double density < 720 sectors, not supported\n" + "Use atrsize to grow the image.\n"); + exit(2); + } else if(dens == DD && size > (128 * 3 + 256 * 717)) { + /* 20071005 bkw: whoops, we were truncating large images to 180K. + The bootloader doesn't work with MyDOS >180K formats anyway, so + don't try. + fprintf(stderr, SELF + ": ATR file is double density > 720 sectors; some files may not" + "appear in the menu or load correctly\n"); + */ + + /* Abort instead */ + fprintf(stderr, SELF + ": ATR is double density > 720 sectors, not supported " + "by bootloader (try MyPicoDOS)\n"); + exit(2); + } + + /* Input looks OK, open the output... */ + if(strcmp(outfile, "-") != 0) { + out = fopen(outfile, "wb"); + if(!out) { + fclose(in); + perror(outfile); + exit(1); + } + } else if(isatty(fileno(stdout))) { + /* don't scare the n00bs! */ + fprintf(stderr, + SELF ": Standard output is a terminal, not writing binary data.\n" + "Either redirect to a file or set the output filename.\n"); + exit(1); + } + + /* write ATR header */ + res = fwrite(buf, 1, 16, out); + if(res < 16) { + perror(outfile); + exit(1); + } + + /* read (and ignore) first 3 sectors. A DD image still uses SD + sectors for the first 3 (boot) sectors on the disk. */ + res = fread(buf, 1, 384, in); + if(res < 384) { + perror(infile); + exit(1); + } + + /* Write the loader. For SD disks, this is the whole thing. + For DD disks, this is the first 384 bytes (3 boot sectors), + and we'll have to write the rest of the object code to sector 720 */ + res = fwrite(bin, 1, 384, out); + if(res < 384) { + perror(outfile); + exit(1); + } + + if(dens == SD) { + /* single density can just use a simple copy loop */ + while( (c = getc(in)) != EOF ) + putc(c, out); + } else { + /* double density: copy sectors 4-719 as-is... */ + for(c=4; c<720; c++) { + if(fread(buf, 1, 256, in) < 256) { + if(feof(in)) { + fprintf(stderr, SELF + ": got premature EOF (bad/truncated ATR image).\n"); + } else { + perror(infile); + } + exit(1); + } + + if(fwrite(buf, 1, 256, out) < 256) { + perror(outfile); + exit(1); + } + } + + /* TODO: check the VTOC and/or look for non-zero data in sector + 720, warn the user if the sector was in use. */ + + /* TODO: fix bootloader to work with MyDOS-style sector link bytes. + Also, store last part of bootloader somewhere not used by MyDOS, + maybe sector 369 (last directory sector, unused on disks with + less than 56 files... and 56 files is way too many to fit on + screen in GR.1). This should happen on both single and double + density images. */ + + /* TODO: examine directory sectors, look for: + - DOS 2.5 extended files. Either warn about them, or clear the + extended flag (which causes the bootloader to load them just fine). + - MyDOS subdirectories. It's probably best to abort in that case. + - Non-DOS-compatible disk formats (boot disks, SpartaDOS, Atari + DOS 3 or 4). + */ + + /* TODO: add option to delete DOS.SYS and DUP.SYS (or more likely, + delete them by default, and add option to allow user to keep them). + */ + + /* TODO: option that creates a new, blank image, with bootloader + already on it? There's already "atrsize -b" for creating a blank + image... */ + + /* write the rest of the loader code code to sector 720. + The code in the boot sectors will load sector 720. */ + if(fwrite(bin+384, 1, 256, out) < 256) { + perror(outfile); + exit(1); + } + } + + /* set return value: 0 for success, 1 for failure */ + c = 0; + + if(ferror(in)) { + perror(infile); + c = 1; + } + + if(ferror(out)) { + perror(outfile); + c = 1; + } + + /* ...and I'm spent! */ + return c; +} diff --git a/fenders.dasm b/fenders.dasm new file mode 100644 index 0000000..7bec9bc --- /dev/null +++ b/fenders.dasm @@ -0,0 +1,570 @@ + +; Fender's 3-sector loader (boot code only) + +; A "game DOS" binary file loader. Install it on a disk full of binaries, +; it replaces the DOS boot loader. You don't need DOS.SYS or DUP.SYS on +; such a disk, so there's more room for games. When you boot, the 3-sector +; loader reads the directory and presents you with a menu of up to 20 +; files to load with a single keypress. + +; presumably written by someone named Fender, in the early 1980s + +; I have no idea what the licensing is (shareware, public domain, ???), +; but after all this time, I don't think anyone's going to sue me. + +; Disassembled with dis6502 +; Commented, labelled, and reformatted for DASM use by B. Watson, 20061105 + +; Can be assembled to a 384-byte object file with a command like: + +; dasm fenders.dasm -ofenders.obj -f3 + +; The object code can be installed on an existing single-density ATR +; floppy image on UN*X like so: + +; dd if=fenders.obj of=disk.atr bs=1 count=384 seek=16 conv=notrunc + +; (for an XFD image, leave off the "seek=16") + +; This is a standalone source file: it doesn't require any "system equates" +; include file. + +; Intended for use with DASM, but doesn't use macros or other fancy +; features, so it'll probably assemble on most 6502 assemblers. +; Your assembler may use a slightly different syntax for conditional +; assembly, if so search the file for ".if" and change as needed. +; Also, remove the "processor" line if you aren't using DASM: + + processor 6502 + +; This disassembly only contains the boot loader code, not the installer. +; The installer is 30 sectors long (10x the size of the loader), and +; pretty straightforward. + +; The installer allows you to customize the loader somewhat. It does +; this by changing the boot loader code before writing it. The options +; in the installer are: + +; Option | Label +; A. Cause COLDSTART on RESET | COLDSTART_ON_RESET +; B. Rotate color during load | SCREEN_OFF [*] +; C. Screen off after load | ROTATE_COLOR [*] +; D. Title | (see "screen" label near end of this file) +; F. Change density | DENSITY (TODO: support this) +; (the other options don't change the boot code) + +; [*] B and C options are mislabelled in the installer, selecting +; "Rotate color" actually toggles "Screen off", and vice versa. + +; Options A, B, and C cause the installer to modify the in-memory copy +; of the boot code before writing it to disk. I've used conditional +; assembly to simulate this: + +COLDSTART_ON_RESET .equ 0 ; 0=false, non-zero=true +ROTATE_COLOR .equ 0 ; 0=false, non-zero=true +SCREEN_OFF .equ 0 ; 0=false, non-zero=true + +; TODO: support conditional assembly for double density. +; Unlike the first 3 options, double density changes a lot of code. +; The original author doesn't change the code at runtime to support +; double-density; the installer contains complete copies of the +; single and double density boot code. +;DENSITY .equ 1 ; 1=single, 2=double + + +;;; OS equates + +;; OS ROM entry points + +; Question for you Atari SIO and OS experts: why does "Mappping the +; Atari" state that you're supposed to call $E543 for the disk, +; instead of $E459, but the author of this program used $E459 anyway? +SIOV .equ $e459 ; Serial I/O + +COLDSV .equ $e477 ; Cold Start (reboot) +KEYBDV .equ $e420 ; K: handler device table +keyb_get_lo .equ KEYBDV+4 ; pointer to "get byte" routine, minus 1 +keyb_get_hi .equ KEYBDV+5 ; (used by get_key) + + +;; OS zero page +BOOTQ .equ $09 ; Tells OS what to do when RESET is pressed: + ; (0 = reboot, 1 = warmstart, JSR through DOSVEC) + +;; OS and FMS page 2 RAM variables +SDMCTL .equ $022f ; used to turn off the screen when done +SDLSTL .equ $0230 ; display list start pointer +COLDST .equ $0244 ; "coldstart in progress" flag +RUNAD .equ $02e0 ; The run address of the loaded file +INITAD .equ $02e2 ; The init address of the loaded file (can be >1 per file) + +;; DCB, used for sector I/O parameters by SIOV (called by read_sector) +DDEVIC .equ $0300 ; Device serial bus ID (set to $31 by OS disk boot) +DUNIT .equ $0301 ; Drive number (we always use 1) +DCOMND .equ $0302 ; SIO command (set to $52, "Read sector", by OS) +DSTATS .equ $0303 ; Data direction register: $40 for "read" +DBUFLO .equ $0304 ; I/O buffer, lo byte +DBUFHI .equ $0305 ; I/O buffer, hi byte +DTIMLO .equ $0306 ; SIO timeout +DBYTLO .equ $0308 ; number of bytes to transfer (AKA sector size), lo byte +DBYTHI .equ $0309 ; ...hi byte +DAUX1 .equ $030a ; For "Read" command, the sector number (lo byte) +DAUX2 .equ $030b ; ...hi byte + +;; Hardware registers +COLPF1 .equ $d017 ; GTIA, used by read_sector to rotate the title color + +;; Local variables (zero page) +dest_ptr .equ $43 ; (AKA FMSZPG) 2 bytes, init to load address +end_address .equ $45 ; 2 bytes +save_pos .equ $49 ; 1 byte: saves X register while loaded init routine runs +menu_counter .equ $b0 ; tracks how many entries (filenames) are in the menu +dir_sector_lo .equ $b1 ; lo byte of current directory sector + ; (range $69-??, high byte is always 1) +menu_ptr .equ $b2 ; points to screen RAM, for printing filenames + +; Starting sector tables. For each file numbered N on the disk, +; do_dirent saves the low byte of its starting sector at +; start_sector_lo_tbl+N and its high byte at start_sector_hi_tbl+N +; Each table is only 32 bytes, but that's fine as we stop reading the +; directory after 20 files are found (all that will fit on the screen). +start_sector_lo_tbl .equ $c0 +start_sector_hi_tbl .equ $e0 + +;;; Local variables (non zero page) +buffer .equ $0b00 +sector_link_hi .equ $0b7d ; buffer + $7d +sector_link_lo .equ $0b7e ; buffer + $7e +sector_byte_count .equ $0b7f ; buffer + $7f + +;;; Bootable disk image starts here + .org $0700 + +;;; Standard Atari boot disk header (6 bytes) +boot_record: + .byte $00 ; ignored + .byte $03 ; number of sectors to read + .word boot_record ; load address + .word COLDSV ; init address, don't think this gets used + +;;; Actual code starts here +boot_continuation: ; OS starts running our code here. +OFFSET_COLDST_1 equ *-boot_record+1 +OFFSET_COLDST_2 equ *-boot_record+5 + .if COLDSTART_ON_RESET ; installer option A + LDY #$01 ; tell OS to reboot if RESET pressed + STY COLDST ; (coldstart in progress = true) + NOP + .else + LDY #$00 ; tell OS not to reboot if RESET pressed + STY COLDST ; (coldstart in progress = false) + INY + .endif + + ; either way, Y is now 1 + STY BOOTQ ; tell OS we booted successfully + STY DUNIT ; set drive #1 for later SIO use + + DEC DTIMLO ; decrease default disk I/O timeout + + ; set up our custom display list + LDA #<display_list ; 74 J + STA SDLSTL + LDA #>display_list ; 8 . + STA SDLSTL+1 + + ; set up to start reading the directory + LDA #<menu + STA menu_ptr + LDA #>menu + STA menu_ptr+1 + LDA #$69 ; Start loading directory at sector 361 ($0169) + STA dir_sector_lo + +read_dir_sector: + LDA dir_sector_lo + STA DAUX1 + LDA #$01 ; The directory is located at sectors 361-368, + STA DAUX2 ; so the high byte is always 1 + JSR read_sector + INC dir_sector_lo + DEX + + ; X reg is the pointer into the sector buffer, keep that in mind +do_dirent: + LDA buffer,X ; look at status byte + BEQ wait_for_input ; if it's 0, there are no more files, so we're done + BMI next_dirent ; if it's deleted (bit 7 true), skip it + AND #$01 ; open for write if bit 0 set + BNE next_dirent ; If it's open for writing, skip it + INC menu_counter + LDY menu_counter + + ; grab and save starting sector; we're building a table in RAM that + ; holds the starting sector for every file on the disk + LDA buffer+3,X + STA start_sector_lo_tbl,Y + LDA buffer+4,X + STA start_sector_hi_tbl,Y + TYA + + ; draw "A.", "B.", etc (menu choice letters) in the menu + ; (menu_ptr) points to column 0 of the current menu line, and gets + ; 20 added to it each time through the loop (we're using GR.1, which + ; has 20 bytes per screen line). + ; Y is the offset into the current line, AKA the column number + CLC + ADC #$a0 ; GR.2 offset into color 2 alphabet (128+32=160) + LDY #$03 ; Menu letter goes in column 3 of the current menu line + STA (menu_ptr),Y + INY + LDA #$8e ; blue period (no, really, a period that's blue) + STA (menu_ptr),Y ; Store in column 4 + INY + +next_char: + INY + LDA buffer+5,X ; buffer+5 holds the first ATASCII char of the filename + INX + SEC + SBC #$20 ; subtracting 32 makes them come out in color 1 + STA (menu_ptr),Y + CPY #$10 ; done with 16-byte dirent? + BNE next_char ; if not, do next character + + ; Set up pointer for next screen line + CLC + LDA menu_ptr + ADC #$14 ; skip to next GR.1 line ($14 = 20 bytes) + STA menu_ptr + BCC skip_ptr_hi + INC menu_ptr+1 ; inc hi byte if necessary +skip_ptr_hi: + LDA menu_counter + CMP #$14 ; see if the screen is full (max entries = 20) + BEQ wait_for_input ; yep, stop adding entries, or... + +next_dirent: + ; set X to point to the offset of the next dir entry + ; they occur every 16 bytes, so: X = (X mod 16) + 16 + TXA + AND #$f0 + CLC + ADC #$10 + TAX + + ; dirents only take up the first 128 bytes of a dir sector, even + ; on a double density disk, so: + ASL ; see if the high bit it set (A >= 128)... + BCC do_dirent ; no, we're still in the same dir sector, do next entry, or.. + BCS read_dir_sector ; yes, we need to load the next sector + +wait_for_input: + JSR get_key ; get_key waits for the user to press a key and returns its + ; ATASCII value in A + SEC + SBC #$40 ; Convert from ATASCII A-Z to numbers 1-26 + CMP menu_counter ; See if it's one of our valid menu entries + + ; do a "branch if less than or equal" to load_file: + BEQ load_file ; if equal, branch + BCS wait_for_input ; else if greater than, ignore the keypress and + ; wait for the user to press another key + ; else if less than, fall through to load_file + +load_file: ; A register has the file number (1 indexed; there's no file #0) + TAX + INC menu_dlist,X ; change selected entry from GR.1 to GR.2, so the user + ; can see which file he'd selected + + ; get starting sector of this file + LDA start_sector_lo_tbl,X + STA DAUX1 + LDA start_sector_hi_tbl,X + STA DAUX2 + + JSR try_read ; read the first sector... + DEX ; point X at first byte we just read + +; main loop of the program (see "Atari Binary Load Format", near end of file) +read_segment: + ; get the segment load address and save in dest_ptr + JSR get_next_byte + STA dest_ptr + JSR get_next_byte + STA dest_ptr+1 + AND dest_ptr + CMP #$ff ; If we just read two $FF bytes, throw them out + BEQ read_segment ; ...and read the next 2 bytes instead + + ; get the segment end address and save in end_address + JSR get_next_byte + STA end_address + JSR get_next_byte + STA end_address+1 + +load_byte: + JSR get_next_byte ; get next loaded data byte + STA (dest_ptr),Y ; store in destination RAM + INC dest_ptr ; bump destination pointer: lo byte + BNE check_seg_done ; skip the hi byte if the lo byte didn't wrap around + INC dest_ptr+1 ; bump hi byte, if needed + BEQ check_for_init ; stop loading this seg if address wraps $FFFF -> $0000 + ; else fall through to check_seg_done + +check_seg_done: + LDA end_address ; double-byte double compare, to see if we've + CMP dest_ptr ; finished loading all the data in the segment + LDA end_address+1 + SBC dest_ptr+1 ; (SBC instead of CMP because we care about the carry here) + BCS load_byte ; if we're not done, load the next byte of data + +check_for_init: + LDA INITAD + ORA INITAD+1 + BEQ read_segment ; if init addr is 0, we don't have one, go do next seg + STX save_pos ; else save byte position (X reg) and do the init + JSR do_init + LDX save_pos ; init routine returns here, reload byte position + LDY #$00 ; zero out init address so it doesn't run again next time + STY INITAD ; (unless of course the next segment(s) load at INITAD) + STY INITAD+1 + BEQ read_segment ; unconditional branch, go read next segment + + ; JSR do_init simulates indirect JSR +do_init: + JMP (INITAD) + +; WARNING: self-modifying code. read_sector modifies the operand +; of the CPX, setting it to the number of valid data bytes in the +; sector just read. The initial value of $7d represents the number of +; data bytes in a sector that's full of data (125 bytes of actual data). +get_next_byte: + CPX #$7d ; are we done with all the bytes in this sector? + BNE return_next_byte ; if not, go do the next one + + LDA DAUX1 ; have we read the last sector? + ORA DAUX2 ; (if so, the "next sector" stored at DAUX1/2 will be 0) + BNE try_read ; if not, go read the next sector +OFFSET_SCREENOFF equ *-boot_record + .if SCREEN_OFF ; installer option C, "turn off screen after loading" + STA SDMCTL ; turn off the screen (er, why?)... + .else + LDA SDMCTL ; don't turn off the screen + .endif + JMP (RUNAD) ; ...we're done loading, go run the binary! + +read_sector: + LDA #>buffer + STA DBUFHI ; set sector buffer (lo byte presumably defaults to 0?) + LDA #$80 ; sector size for SIO: 128 bytes for single density + STA DBYTLO + +try_read: + LDA #$40 ; data direction = read (bit 6 set, bit 7 clear)... + STA DSTATS ; DSTATS is where we store the data direction + JSR SIOV ; all set up, so call SIO + BMI try_read ; on error, just retry (forever, if need be) + + ; set up SIO sector number for next time: + LDA sector_link_hi ; The high 2 bits of the sector link (AKA next sector in + ; file) are stored in the LOW 2 bits of sector_link_hi, + ; along with the file number in bits 2-7 + AND #$03 ; mask off file number + STA DAUX2 + LDA sector_link_lo ; sector link, low 8 bits + STA DAUX1 + + ; rotate PF color +OFFSET_ROTCOLOR equ *-boot_record + .if ROTATE_COLOR ; installer option B + STA COLPF1 + .else + LDA COLPF1 + .endif + + ; save number of data bytes in sector + LDA sector_byte_count + AND #$7f ; hmm, is this really necessary? + STA get_next_byte+1 ; WARNING: self-modifying code (see above) + + ; init source and destination indices + LDY #$00 + LDX #$00 + +return_next_byte: + LDA buffer,X + INX + RTS + +; Simulate JSR through keyb_get_lo. This is one of those Atari +; "entry point minus one" vectors. To call it, you push it onto the +; stack and do an RTS (works because a real JSR saves the PC minus 1 on +; the stack). +; Becauses it gets the vector from ROM instead of hard-coding it in +; the get_key routine, this code will work on all Atari models (400/800, +; XL/XE). It's a compromise between hardcoding the address and doing +; a full-blown IOCB setup and CIO call. +get_key: + LDA keyb_get_hi + PHA + LDA keyb_get_lo + PHA + RTS + +;;; Data +display_list: ; display list, $084A - $0867 + ; 3 "blank 8 scans", a GR.2 line with LMS, then screen RAM address + .byte $70,$70,$70,$47,<screen,>screen ; "pppGh." +menu_dlist: + ; blank 8 scans, then 20 GR.1 lines, then a jump back to the + ; start of the display list. + .byte $70,$06,$06,$06,$06,$06,$06,$06 ; "p......." + .byte $06,$06,$06,$06,$06,$06,$06,$06 ; "........" + .byte $06,$06,$06,$06,$06,$41,<display_list,>display_list ; ".....AJ." + + ; screen RAM, $0868 - $087F and beyond +OFFSET_TITLE equ *-boot_record +screen: + ; $00 is a space, lowercase letters come out as caps (color reg 1) + .byte $00,$00,$00,$00 ; 4 spaces for centering + .byte "atari", $00, "arcade" ; "ATARI ARCADE" in green + .byte $00,$00,$00,$00 ; rest of the GR.2 line (20 bytes total) +menu: ; the filenames start getting stored here, 2nd line of the display + + ; 4 filler bytes, so we come out at an even 3 sectors (384 bytes) + .byte $00,$00,$00,$00 + +;;; End of code. Rest of file is boring documentation :) + +; Atari Binary Load File Format - A Primer + +; The binary load format was first defined for Atari DOS 1.0, and supported +; by all DOSes for the Atari. There's no formal standard that I'm aware of; +; the closest thing is probably the binary load implementation in Atari +; DOS 2.0S or 2.5 (the most widely-used DOSes from Atari), but that's +; copyrighted code, so it couldn't be used as a base to build on or as a +; module for another DOS. + +; Sadly, Atari didn't add a CIO SPECIAL (aka XIO from BASIC) command to load +; binary files as part of their DOS's public API... so anyone who wanted to +; write a game-loader or menu that runs under DOS would have to write their +; own loader (for compatibility with many DOSes), or just assume they'll +; be running under DOS 2 and JSR to the unpublished entry point within DOS +; (which changed between DOS 2 and 2.5). Some third-party DOSes (SpartaDOS, +; MyDOS) included a CIO/XIO call for loading and/or running binary files, +; which meant a menu program was easy to write in just a few lines of +; BASIC, but it wouldn't work on standard Atari DOS, and might not even +; be portable from Sparta to MyDOS, if they didn't choose the same CIO +; command for binary loading. + +; None of this DOS-related discussion is relevant to Fenders, though. +; The whole point of Fenders is to do away with the overhead of DOS, so: + +; - You don't have to keep copies of DOS.SYS (39 sectors) or DUP.SYS (42 +; sectors) on your disk. Together these take up over 10% of the available +; storage on a DOS 2 formatted disk. With Fenders you get all 707 sectors +; to use for game storage. + +; - You don't have to wait for a full-blown DOS boot. It takes a while +; to load DOS.SYS and DUP.SYS (81 sectors, taken together), which are a +; rather complete file management system and menu interface that you don't +; need for playing games. With Fenders, you get your menu almost instantly. + +; - Once DOS and DUP are loaded, you have to type "A Return Return" to +; get a disk directory, then "L Return FILENAME Return" to load a file. +; With Fenders, you type one letter. + +; There is no support in the OS ROM for the binary load format, or for +; the disk device at all at the CIO level (the ROM does have SIO routines +; for reading/writing raw sectors, which is what Fenders uses). There are +; many binary loader implementations on the Atari. Each third-party DOS +; generally had to roll its own, and there were numerous "No DOS" and +; "Game DOS" utilities (like Fenders) that had to implement read-only +; support for the DOS 2 filesystem along with a binary loader. Fenders +; does this in 330 bytes of code (including the user interface). + +; An Atari binary load file consists of one or more segments, each with +; its own load address and end address. + +; A segment is: + +; FFFF leader (bytes 0 and 1) - A two-byte leader: $FF, $FF +; Required for 1st segment, optional for others +; Load address (bytes 2 and 3 in standard 6502 LSB/MSB) +; End address (bytes 4 and 5) +; (Load address - End address + 1) bytes of data to be loaded + +; Fenders actually doesn't require the FFFF leader even for the first +; segment, and could deal with multiple FFFF leaders if present: it +; just skips over them. I haven't checked to see if Atari DOS does +; the same thing or not. + +; A well-formed binary load file must have an end address higher than the +; start address for every segment. It's undefined what happens otherwise +; (Fenders quits loading if the current address ever wraps around from +; $FFFF to 0000, I don't know what it does if the end address is the same +; as the start address). + +; There's no rule against having overlapping segments, but it's kind +; of an odd thing to do. + +; After each segment is loaded, a loader must check INITAD (a 16-bit +; vector at location $02E2) to see if the segment loaded anything at +; that address. If so, a JSR through the vector is performed, with the +; expectation that the loaded program's initialization routine will run +; and then do an RTS to return control to the loader. This is often used +; (or abused) by crackers to load a screen with their name/logo. In a +; real DOS, IOCB #1 is left open, so theoretically the init routine could +; read data from the file, located between segments as it were. Fenders +; doesn't do this, as it doesn't implement a CIO handler at all. Any program +; that relies on this behavior won't work with Fenders, which isn't much +; of a limitation (I doubt this capability has ever been used in the +; entire history of the Atari. It might be useful for something like an +; auto-relocating load?). Use of the init vector is optional. Since it's +; checked after every segment, there can be more than one init routine +; run this way. + +; After all segments have been loaded, a loader must check RUNAD (16-bit +; vector located at $2E0) to see if any segment loaded anything at that +; address. If so, a JSR through the vector is performed to run the main +; routine of the just-loaded program, which may or may not do an RTS to +; return control to the caller. In a real DOS, the RTS will generally redraw +; the DOS menu or print a new command prompt (whatever makes sense). Fenders +; does a JMP (RUNAD), not expecting the loaded program to return (most +; games just run forever). + +; If RUNAD never gets loaded, a real DOS will return to its menu or prompt +; without trying to run anything. You could use this to just load some +; data (a font, maybe) into memory. Fenders will crash if RUNAD never gets +; loaded, since it's set to 0 by the OS (a JMP (0) happens, transferring +; control off to never-never-land). Again, not much of a limitation, since +; it's intended for running games, not general-purpose data-file loading. + +; There is ABSOLUTELY NO protection against a binary file trying to +; load on top of Fenders while it's running (or the memory-mapped +; I/O registers, or zero page, or anywhere it wants to). There is also +; no protection against a loaded init routine messing up the zero page +; pointers or disk buffer. In practice, this doesn't happen often +; (and a "rude" file that did this would probably have problems loading +; under a standard DOS, too). + +; This information comes from various sources. I probably gleaned most of +; it from "Mapping the Atari" or the Compute! magazine "Insight: Atari" +; column, way back when. Both of these are available on the web now: + +; Mapping the Atari, De Re Atari, and a lot of other books can be +; found at http://www.atariarchives.org + +; Most of the old Compute! and Antic magazines have been scanned and +; archived at http://www.atarimagazines.com/ + +; There's also a "Digital ANALOG Project" that's got a lot of Analog +; magazine issues archived: http://www.cyberroach.com/analog/ + +; For a compact reference to the binary load format, see: +; http://www.atarimax.com/jindroush.atari.org/afmtexe.html + +; TODO: I want to add high speed I/O to Fenders, for use with the SIO2PC +; and AtariSIO (or APE). + diff --git a/fenders.rst b/fenders.rst new file mode 100644 index 0000000..9c4f1c7 --- /dev/null +++ b/fenders.rst @@ -0,0 +1,239 @@ +.. RST source for fenders(1) man page. Convert with: +.. rst2man.py fenders.rst > fenders.1 +.. rst2man.py comes from the SBo development/docutils package. + +======= +fenders +======= + +--------------------------------------------------------------- +Install Fenders 3-sector loader in boot sectors of an ATR image +--------------------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +*fenders* [*-hrcsiv*] [-t *title*] *infile.atr* [*outfile.atr*] + +DESCRIPTION +=========== + +**fenders** replaces the boot sectors of an ATR image with a menu-driven +"game dos" binary loader. + +When the disk is booted, a disk directory menu is displayed on the +screen. To load a binary (XEX/COM/BIN/etc) file, press the letter +shown for that file. Other file types may not be loaded (and will cause +the Atari to crash). + +The installed bootloader overwrites sectors 1-3 of the image +(and sector 720, for double-density images). This is the same code +installed by the Atari-based **Fenders 3-sector** loader installer +utility. + + +OPTIONS +======= + +-h + Print this help message. + +-r + DON'T reboot (coldstart) the Atari if Reset is pressed. The default is to coldstart. + +-c + Rotate colors during load. May cause problems with some games. + +-s + Screen off after load. May cause problems with some games, but + may fix graphics corruption for other games. YMMV. + +-i + In-place update. The input file is renamed to end in **~** (tilde), + and the output is written to the original filename. May not be + used when reading from standard input. + +-v + Set inverse video bit in title. This causes the title text to + appear in red and/or blue on the Atari, instead of the default + orange and green colors. + +-t + Set the menu title. May up to 20 characters; default is *atari + arcade*. Will be truncated to 20 characters if a longer title is + given. See MENU TITLE, below. Note that you'll have to quote the + title if it contains spaces or other characters that have meaning to your shell. + +infile + The ATR image to read from. It must be either single-density + and at least 368 sectors long, or double-density and at least + 720 sectors long. You may use **-** for *infile* to read from + standard input (unless **-i** is used). + +outfile + The ATR image to create, which will contain all the files + from the input image, plus the Fenders boot loader. You may + omit *outfile* or use **-** to write to standard output. *outfile* is + ignored when the **-i** option is used. + +If both *infile* and *outfile* are omitted, the default is to read from +standard input and write to standard output. + +NOTES +===== + +**fenders** will abort, if asked to write to standard output when standard +output is a terminal. This is to avoid spewing binary garbage to your +terminal. + +The **-r**, **-c**, **-s**, and **-t** options actually modify the **fenders** 6502 object +code before writing it to the ATR image. + +The Atari **fenders** installer's default is to not coldstart, which +tends to cause the Atari to lock up when Reset is pressed. The +author believes that rebooting the Atari is more useful than locking +it up, so **fenders** causes the coldstart by default. + +The Atari **fenders** installer contains a bug: the meanings of the "Rotate +color" and "Screen off after load" options are reversed. **fenders** does +not duplicate this bug. + +The disk density doesn't have to be specified, because **fenders** reads it +from the ATR header. The loader's 6502 object code is different, for +single- and double-density disks. + +The double-density version of the loader isn't actually a 3-sector +loader. The first 384 bytes of its code are stored in the 3 boot sectors, and the last 256 bytes of code are actually read from sector 720. +When installing the boot loader on a double-density disk, sector 720 is +**overwritten**. Any data that may have been there is lost. On most disks, +sector 720 is either unusable by DOS, or not used unless the disk is +completely full, so this is less of a problem than you might think. + +**fenders** and the **fenders** boot-loader code will work with DOS +2.5 "enhanced density" formatted floppies, but only partially: files +that use sectors above 720 will not appear in the menu (these are the +same files that DOS 2.5 lists with <> around the filename). + +**fenders** only works on Atari DOS 2.x and compatible (MyDOS, DOS XL, et +al) single-sided disk images, either single-density (720 sectors, 90K), +double-density (720 sectors, 180K), or "1050 enhanced" density (1040 +sectors, 130K, although 1050 enhanced density images must be in DOS 2.5 +format, *not* MyDOS, and see **LIMITATIONS** below). Other non-standard DOS +formats such as SpartaDOS or Atari DOS 3.0 and 4.0 are not supported. +Atari DOS 1.0 may or may not work (untested). + +The **fenders** boot loader source code distributed with **fenders** +is not the original source code (which has never been +released). The author of **fenders** spent a couple of days with a +disassembly of the object code and reverse engineered (labelled, +commented) it so that humans can read it, provided they are humans who +speak 6502 assembly. The *fenders.dasm* and *fendersdbl.dasm* files +can be assembled with the DASM cross assembler. The resulting object +code is identical with the original **fenders** object code (which +the author read from a disk image in the first place). + +Contrary to what you may have read, the Fenders boot loader code +doesn't have to be rewritten to a disk if you add/delete files. It +reads the disk directory, and can cope with changes just fine. Even a +heavily fragmented filesystem won't cause any problems. + +MENU TITLE +========== + +The menu title (set with **-t**) is stored in sector 3 for a +single-density disk or sector 720 of a double-density disk, in Atari +internal screen codes (which are not the same as either ASCII or +ATASCII). fenders converts the title into internal codes, so the user +doesn't have to worry about this. + +The menu title is displayed in "GRAPHICS 2" mode, which can only +display 64 different characters, including uppercase, numbers, and +(most) punctuation, but NOT including lowercase or inverse video. + +However, lowercase letters are displayed as different-colored +uppercase letters, and inverse characters are also displayed with +different colors. The boot loader uses the default Atari +colors, as set by the Atari OS. + +The title is limited to 20 characters because that's the width of a +GRAPHICS 2 line of text on the Atari. + +The character set used by GRAPHICS 2 consists of: + +- The space character. + +- The letters **A-Z**. + +- The numbers **0-9**. + +- Punctuation: **!** **"** **#** **$** **%** **&** **'** **(** **)** **\*** **+** **,** **-** **.** **/** **:** **;** **<** **=** **>** **?** **@** **[** **\\** **]** **^** **_** + +Uppercase letters, numbers, and punctuation are displayed in orange (or +blue, if **-v** is used). + +Lowercase letters are displayed in green (or red, with **-v**). + +The characters ` { | } ~ are displayed as green (or red) versions of +@ [ \ ] ^, respectively. + +Currently, it's not possible to mix normal (orange/green) characters +with inverse (blue/red) with fenders, unless your terminal provides a +way for you to enter ASCII characters with their high bits set. Some +versions of xterm(1x) allow this with the Alt or Meta key. + +LIMITATIONS +=========== + +**fenders** should warn if the disk image contains more than 20 files, +since they won't all be displayed in the menu. + +Double-density images less than 720 sectors long are not handled, +although they could be with a little more work. + +For double-density disks, the VTOC and sector link bytes should +be checked to see if sector 720 is in use, rather than just blindly +overwriting it. On a single-density DOS 2.0S disk, sector 720 is +marked "in use" in the VTOC when the disk is formatted, but will never +be used for file storage. + +No checking for non-standard formats (SpartaDOS, DOS 3, MyDOS with +subdirectories, dedicated bootdisks, etc) is done. + +If an "enhanced" 1050 density disk image has the bootloader installed, +it won't display the files using sectors above 720 (the ones which +would appear as *<filename.ext>* in the DOS 2.5 directory). + +Actually, the above limitations are a direct result of the fact that +**fenders** deals with the disk image at the "sector" level, and contains +no code that understands the files, directory, or VTOC on the image. +The original Atari-based Fenders installer shares the same limitations, +so the author doesn't really consider them to be bugs, although they +may be addressed in a future version of **fenders**. + +Note that the bootloader only displays up to 20 files on the screen. +This shouldn't be a real problem (how many games can you fit on a 180K +floppy?), but no warning or error is given if there are too many files. + +The bootloader only supports standard SIO disk speed (19200bps). The +only way to make it support high-speed SIO is to use the APE Warp OS +(or some other high-speed patched OS) on the Atari. + +BUGS +==== + +There should be an option to delete DOS.SYS and DUP.SYS from the image, +but there isn't. The original Fenders installer has this option. + +When used with an image whose size according to the ATR header +doesn't match the actual image file size, **fenders** may produce +a broken or zero-length output file. If in doubt, use **atrcheck** to +validate the image before use. + +There should be a way to install an arbitrary 3-sector (384-byte) +binary file in the boot sectors of an image. This can be done with +**dd**, of course, but its syntax is difficult to remember, and typos are +prone to wipe out the file you're working with. + +.. include:: manftr.rst diff --git a/fenders_bin.c b/fenders_bin.c new file mode 100644 index 0000000..8256bb8 --- /dev/null +++ b/fenders_bin.c @@ -0,0 +1,54 @@ +/* C source created by blob2c from input file fenders.bin */ + +unsigned char fenders_bin[] = { + /* 0 */ 0x00,0x03,0x00,0x07,0x77,0xe4,0xa0,0x00, /* ....w... */ + /* 8 */ 0x8c,0x44,0x02,0xc8,0x84,0x09,0x8c,0x01, /* .D...... */ + /* 16 */ 0x03,0xce,0x06,0x03,0xa9,0x4a,0x8d,0x30, /* .....J.0 */ + /* 24 */ 0x02,0xa9,0x08,0x8d,0x31,0x02,0xa9,0x7c, /* ....1..| */ + /* 32 */ 0x85,0xb2,0xa9,0x08,0x85,0xb3,0xa9,0x69, /* .......i */ + /* 40 */ 0x85,0xb1,0xa5,0xb1,0x8d,0x0a,0x03,0xa9, /* ........ */ + /* 48 */ 0x01,0x8d,0x0b,0x03,0x20,0x0b,0x08,0xe6, /* .... ... */ + /* 56 */ 0xb1,0xca,0xbd,0x00,0x0b,0xf0,0x4f,0x30, /* ......O0 */ + /* 64 */ 0x41,0x29,0x01,0xd0,0x3d,0xe6,0xb0,0xa4, /* A)..=... */ + /* 72 */ 0xb0,0xbd,0x03,0x0b,0x99,0xc0,0x00,0xbd, /* ........ */ + /* 80 */ 0x04,0x0b,0x99,0xe0,0x00,0x98,0x18,0x69, /* .......i */ + /* 88 */ 0xa0,0xa0,0x03,0x91,0xb2,0xc8,0xa9,0x8e, /* ........ */ + /* 96 */ 0x91,0xb2,0xc8,0xc8,0xbd,0x05,0x0b,0xe8, /* ........ */ + /* 104 */ 0x38,0xe9,0x20,0x91,0xb2,0xc0,0x10,0xd0, /* 8. ..... */ + /* 112 */ 0xf2,0x18,0xa5,0xb2,0x69,0x14,0x85,0xb2, /* ....i... */ + /* 120 */ 0x90,0x02,0xe6,0xb3,0xa5,0xb0,0xc9,0x14, /* ........ */ + /* 128 */ 0xf0,0x0c,0x8a,0x29,0xf0,0x18,0x69,0x10, /* ...)..i. */ + /* 136 */ 0xaa,0x0a,0x90,0xae,0xb0,0x9c,0x20,0x41, /* ...... A */ + /* 144 */ 0x08,0x38,0xe9,0x40,0xc5,0xb0,0xf0,0x02, /* .8.@.... */ + /* 152 */ 0xb0,0xf4,0xaa,0xfe,0x50,0x08,0xb5,0xc0, /* ....P... */ + /* 160 */ 0x8d,0x0a,0x03,0xb5,0xe0,0x8d,0x0b,0x03, /* ........ */ + /* 168 */ 0x20,0x15,0x08,0xca,0x20,0xf9,0x07,0x85, /* ... ... */ + /* 176 */ 0x43,0x20,0xf9,0x07,0x85,0x44,0x25,0x43, /* C ...D%C */ + /* 184 */ 0xc9,0xff,0xf0,0xf0,0x20,0xf9,0x07,0x85, /* .... ... */ + /* 192 */ 0x45,0x20,0xf9,0x07,0x85,0x46,0x20,0xf9, /* E ...F . */ + /* 200 */ 0x07,0x91,0x43,0xe6,0x43,0xd0,0x04,0xe6, /* ..C.C... */ + /* 208 */ 0x44,0xf0,0x0a,0xa5,0x45,0xc5,0x43,0xa5, /* D...E.C. */ + /* 216 */ 0x46,0xe5,0x44,0xb0,0xe9,0xad,0xe2,0x02, /* F.D..... */ + /* 224 */ 0x0d,0xe3,0x02,0xf0,0xc7,0x86,0x49,0x20, /* ......I */ + /* 232 */ 0xf6,0x07,0xa6,0x49,0xa0,0x00,0x8c,0xe2, /* ...I.... */ + /* 240 */ 0x02,0x8c,0xe3,0x02,0xf0,0xb6,0x6c,0xe2, /* ......l. */ + /* 248 */ 0x02,0xe0,0x7d,0xd0,0x3f,0xad,0x0a,0x03, /* ..}.?... */ + /* 256 */ 0x0d,0x0b,0x03,0xd0,0x10,0xad,0x2f,0x02, /* ....../. */ + /* 264 */ 0x6c,0xe0,0x02,0xa9,0x0b,0x8d,0x05,0x03, /* l....... */ + /* 272 */ 0xa9,0x80,0x8d,0x08,0x03,0xa9,0x40,0x8d, /* ......@. */ + /* 280 */ 0x03,0x03,0x20,0x59,0xe4,0x30,0xf6,0xad, /* .. Y.0.. */ + /* 288 */ 0x7d,0x0b,0x29,0x03,0x8d,0x0b,0x03,0xad, /* }.)..... */ + /* 296 */ 0x7e,0x0b,0x8d,0x0a,0x03,0xad,0x17,0xd0, /* ~....... */ + /* 304 */ 0xad,0x7f,0x0b,0x29,0x7f,0x8d,0xfa,0x07, /* ...).... */ + /* 312 */ 0xa0,0x00,0xa2,0x00,0xbd,0x00,0x0b,0xe8, /* ........ */ + /* 320 */ 0x60,0xad,0x25,0xe4,0x48,0xad,0x24,0xe4, /* `.%.H.$. */ + /* 328 */ 0x48,0x60,0x70,0x70,0x70,0x47,0x68,0x08, /* H`pppGh. */ + /* 336 */ 0x70,0x06,0x06,0x06,0x06,0x06,0x06,0x06, /* p....... */ + /* 344 */ 0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06, /* ........ */ + /* 352 */ 0x06,0x06,0x06,0x06,0x06,0x41,0x4a,0x08, /* .....AJ. */ + /* 360 */ 0x00,0x00,0x00,0x00,0x61,0x74,0x61,0x72, /* ....atar */ + /* 368 */ 0x69,0x00,0x61,0x72,0x63,0x61,0x64,0x65, /* i.arcade */ + /* 376 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 /* ........ */ +}; /* fenders_bin */ + +int fenders_bin_len = 384; diff --git a/fenders_bin.h b/fenders_bin.h new file mode 100644 index 0000000..3482b5b --- /dev/null +++ b/fenders_bin.h @@ -0,0 +1,9 @@ +/* C header created by blob2c from input file fenders.bin */ + +#ifndef fenders_bin_H +#define fenders_bin_H + +extern unsigned char fenders_bin[]; +extern int fenders_bin_len; + +#endif /* fenders_bin_H */ diff --git a/fenders_offsets.h b/fenders_offsets.h new file mode 100644 index 0000000..bf3431b --- /dev/null +++ b/fenders_offsets.h @@ -0,0 +1,5 @@ +#define OFFSET_COLDST_1 0x0007 +#define OFFSET_COLDST_2 0x000b +#define OFFSET_ROTCOLOR 0x012d +#define OFFSET_SCREENOFF 0x0105 +#define OFFSET_TITLE 0x0168 diff --git a/fenders_offsets.pl b/fenders_offsets.pl new file mode 100644 index 0000000..8ecce64 --- /dev/null +++ b/fenders_offsets.pl @@ -0,0 +1,6 @@ +#!/usr/bin/perl -w + +while(<>) { + chomp; + print "#define $1 0x$2\n" while(/(OFFSET_\w+):?\s+([0-9a-f]+)/g); +} diff --git a/fendersdbl.bin b/fendersdbl.bin Binary files differnew file mode 100644 index 0000000..94c4545 --- /dev/null +++ b/fendersdbl.bin diff --git a/fendersdbl.dasm b/fendersdbl.dasm new file mode 100644 index 0000000..cb6ea3f --- /dev/null +++ b/fendersdbl.dasm @@ -0,0 +1,411 @@ +; Fenders "3-sector" loader disassembly, 20070526 bkw +; Double-density version + +; Note: the double-density loader doesn't actually fit in 3 sectors. +; It uses sectors 1-3 and 720. +; First 3 sectors of a DD disk are still only 128 bytes/sector. + +; At boot, the OS boot code loads the first 3 sectors and jumps to +; the loaded code... which then loads sector 720 (a proper 256-byte DD +; sector), which contains the rest of the code. + +; I haven't done a very thorough job of reverse-engineering the DD +; version of the loader. Its structure is similar to that of the SD +; loader (fenders.dasm). + + processor 6502 + +;;; Equates: + +;; OS ROM entry points +SIOV .equ $e459 +COLDSV .equ $e477 +KEYBDV .equ $e420 ; K: handler device table +keyb_get_lo .equ KEYBDV+4 ; pointer to "get byte" routine, minus 1 +keyb_get_hi .equ KEYBDV+5 ; (used by get_key) + +;; OS zero page +BOOTQ .equ $09 +SAVMSC .equ $58 +ZROFRE .equ $80 + +;; OS and FMS page 2 RAM variables +COLDST .equ $0244 +SDMCTL .equ $022f +SDLSTL .equ $0230 +SDLSTH .equ $0231 +RUNAD .equ $02e0 +INITAD .equ $02e2 + +;; DCB, used for sector I/O parameters by SIOV (called by read_sector) +DDEVIC .equ $0300 +DUNIT .equ $0301 +DCOMND .equ $0302 +DSTATS .equ $0303 +DBUFLO .equ $0304 +DBUFHI .equ $0305 +DTIMLO .equ $0306 +DBYTLO .equ $0308 +DBYTHI .equ $0309 +DAUX1 .equ $030a +DAUX2 .equ $030b + +;; Hardware registers +COLPF1 .equ $d017 +VCOUNT .equ $d40b + +;; Local variables (zero page) +end_address .equ $45 +save_pos .equ $49 +menu_counter .equ $b0 +dir_sector_lo .equ $b1 +tmp_dlistl .equ $b2 +tmp_dlisth .equ $b3 +menu_ptr_lo .equ $b4 +menu_ptr_hi .equ $b5 +dest_ptr .equ $43 +start_sector_lo_tbl .equ $c0 +start_sector_hi_tbl .equ $e0 + +;; Local variables (non zero page) +buffer .equ $0b10 +sector_link_hi .equ $0c0d ; buffer + $fd +sector_link_lo .equ $0c0e ; buffer + $fe +sector_byte_count .equ $0c0f ; buffer + $ff + +;;; Bootable disk image starts here: + .org $0700 + +;;; Standard Atari boot disk header (6 bytes) +boot_record: + .byte $00 ; ignored + .byte $03 ; number of sectors to read + .word boot_record ; load address + .word COLDSV ; init address, don't think this gets used + +;;; Actual code starts here: +boot_continuation: +OFFSET_COLDST_1_DD .equ *-boot_record+1 +OFFSET_COLDST_2_DD .equ *-boot_record+5 + LDY #$00 ; 0 . + STY COLDST + INY + STY BOOTQ + STY DUNIT + DEC DTIMLO +set_dbl_density: + LDA #$4e ; 78 N + STA DCOMND + LDA #$40 ; 64 @ + STA DSTATS + LDA #$0c ; 12 . + STA DBYTLO + LDA #$00 ; 0 . + STA DBYTHI + LDA #<buffer + STA DBUFLO + LDA #>buffer + STA DBUFHI + JSR SIOV + BMI set_dbl_density + LDA #$04 ; 4 . + STA buffer+5 + LDA #$01 ; 1 . + STA buffer+6 + LDA #$00 ; 0 . + STA buffer+7 + LDA #$4f ; 79 O + STA DCOMND + LDA #$80 ; 128 . + STA DSTATS + JSR SIOV + BMI set_dbl_density + +; The bootloader code is 640 bytes long. First 384 bytes were loaded +; from the 3 boot sectors already; the rest lives in sector 720, which +; we have to load before running it: +read_sec_720: + LDA #$52 ; 82 R + STA DCOMND + LDA #$40 ; 64 @ + STA DSTATS + LDA #$80 ; 128 . + STA DBUFLO + LDA #$08 ; 8 . + STA DBUFHI + LDA #$00 ; 0 . + STA DBYTLO + LDA #$01 ; 1 . + STA DBYTHI + LDA #$d0 ; 208 . + STA DAUX1 + LDA #$02 ; 2 . + STA DAUX2 + JSR SIOV + BMI read_sec_720 + + ; setup display list + LDA SDLSTL + STA tmp_dlistl + LDA SDLSTH + STA tmp_dlisth + LDA #$00 ; 0 . + STA SDMCTL + LDA #<display_list ; 58 : + STA SDLSTL + LDA #>display_list ; 9 . + STA SDLSTH + + ; init menu + LDA #$6c ; 108 l + STA menu_ptr_lo + LDA #$09 ; 9 . + STA menu_ptr_hi + LDA #$69 ; 105 i + STA dir_sector_lo + +read_dir_sector: + LDA dir_sector_lo + STA DAUX1 + LDA #$01 ; 1 . + STA DAUX2 + JSR read_sector + INC dir_sector_lo + DEX + +do_dirent: + LDA buffer,X + BEQ dir_done + BMI next_dirent + AND #$01 ; 1 . + BNE next_dirent + INC menu_counter + LDY menu_counter + LDA buffer+3,X + STA start_sector_lo_tbl,Y + LDA buffer+4,X + STA start_sector_hi_tbl,Y + TYA + CLC + ADC #$a0 ; 160 . + LDY #$03 ; 3 . + STA (menu_ptr_lo),Y + INY + LDA #$8e ; 142 . + STA (menu_ptr_lo),Y + INY +next_char: + INY + LDA buffer+5,X + INX + SEC + SBC #$20 ; 32 + STA (menu_ptr_lo),Y + CPY #$10 ; 16 . + BNE next_char + CLC + LDA menu_ptr_lo + ADC #$14 ; 20 . + STA menu_ptr_lo + BCC skip_ptr_hi + INC menu_ptr_hi +skip_ptr_hi: + LDA menu_counter + CMP #$14 ; 20 . + BEQ dir_done +next_dirent: + TXA + AND #$f0 ; 240 . + CLC + ADC #$10 ; 16 . + TAX + ASL + BCC do_dirent + BCS read_dir_sector +dir_done: + LDA #$22 ; 34 " + STA SDMCTL +wait_vcount_0: + LDA VCOUNT + BNE wait_vcount_0 +wait_for_input: + JSR get_key + SEC + SBC #$40 ; 64 @ + CMP menu_counter + BEQ load_file + BCS wait_for_input +load_file: + TAX + LDA start_sector_lo_tbl,X + STA DAUX1 + LDA start_sector_hi_tbl,X + STA DAUX2 + LDA #$68 ; 104 h + STA menu_ptr_lo + LDA #$09 ; 9 . + STA menu_ptr_hi +L0834: + DEX + BEQ print_loading_msg + CLC + LDA menu_ptr_lo + ADC #$14 ; 20 . + STA menu_ptr_lo + BCC L0834 + INC menu_ptr_hi + BNE L0834 +print_loading_msg: + LDY #$00 ; 0 . +next_msg_byte: + LDA loading_msg,Y + STA (SAVMSC),Y + INY + CPY #$09 ; 9 . + BNE next_msg_byte +print_filename: + LDA (menu_ptr_lo),Y + STA (SAVMSC),Y + INY + CPY #$15 ; 21 . + BNE print_filename + LDA #$00 ; 0 . + STA SDMCTL + LDA tmp_dlistl + STA SDLSTL + LDA tmp_dlisth + STA SDLSTH + LDA #$22 ; 34 " + STA SDMCTL +wait_vcount_again: + LDA VCOUNT + BNE wait_vcount_again + LDY #$00 ; 0 . + TYA +clear_zp: + STA ZROFRE,Y + INY + BPL clear_zp + +; the "JSR try_read" below MUST be located at $087f. +; The JSR opcode is the last byte loaded in the 3-sector boot loader, +; and its operand is the first 2 bytes loaded from sector 720! + .if *<>$087f + .echo "Code offsets have changed, fix me (", *, "should be $087f)" + .err + .endif + JSR try_read ; cut here! + + DEX + +read_segment: + JSR get_next_byte + STA dest_ptr + JSR get_next_byte + STA dest_ptr+1 + AND dest_ptr + CMP #$ff ; 255 . + BEQ read_segment + JSR get_next_byte + STA end_address + JSR get_next_byte + STA end_address+1 +load_byte: + JSR get_next_byte + STA (dest_ptr),Y + INC dest_ptr + BNE check_seg_done + INC dest_ptr+1 + BEQ check_for_init +check_seg_done: + LDA end_address + CMP dest_ptr + LDA end_address+1 + SBC dest_ptr+1 + BCS load_byte +check_for_init: + LDA INITAD + ORA INITAD+1 + BEQ read_segment + STX save_pos + JSR do_init + LDX save_pos + LDY #$00 ; 0 . + STY INITAD + STY INITAD+1 + BEQ read_segment + +do_init: + JMP (INITAD) + +get_next_byte: + ; self-modifying code changes immediate CPX operand + CPX #$fd ; 253 . + BNE return_next_byte + LDA DAUX1 + ORA DAUX2 + BNE try_read +OFFSET_SCREENOFF_DD .equ *-boot_record + LDA SDMCTL + JMP (RUNAD) + +read_sector: + LDA #$31 ; 49 1 + STA DDEVIC + LDA #$52 ; 82 R + STA DCOMND + LDA #<buffer ; 16 . + STA DBUFLO + LDA #>buffer ; 11 . + STA DBUFHI + LDA #$00 ; 0 . + STA DBYTLO + LDA #$01 ; 1 . + STA DBYTHI + +try_read: + LDA #$40 ; 64 @ + STA DSTATS + JSR SIOV + BMI try_read + LDA sector_link_hi + AND #$03 ; 3 . + STA DAUX2 + LDA sector_link_lo + STA DAUX1 +OFFSET_ROTCOLOR_DD .equ *-boot_record + LDA COLPF1 + LDA sector_byte_count + STA get_next_byte+1 + LDY #$00 ; 0 . + LDX #$00 ; 0 . +return_next_byte: + LDA buffer,X + INX + RTS + +get_key: + LDA keyb_get_hi + PHA + LDA keyb_get_lo + PHA + RTS + +loading_msg: + .byte $00,$00,$2c,$6f,$61,$64,$69,$6e ; "..,oadin" + .byte $67,$00,$00 ; "g.." +display_list: + .byte $70,$70,$70,$47 ; "pppG" + .byte <screen,>screen + .byte $70,$06,$06,$06,$06,$06,$06 ; "p......" + .byte $06,$06,$06,$06,$06,$06,$06,$06 ; "........" + .byte $06,$06,$06,$06,$06,$06,$41 ; "......A" + .byte <display_list,>display_list +OFFSET_TITLE_DD .equ *-boot_record +screen: + .byte $00,$00,$00,$00,$61,$74,$61 ; ".....ata" + .byte $72,$69,$00,$61,$72,$63,$61,$64 ; "ri.arcad" + .byte $65,$00,$00,$00,$00,$00,$00,$00 ; "e......." + .byte $00,$00,$00,$00,$00,$00,$00,$00 ; "........" + .byte $00,$00,$00,$00,$00,$00,$00,$00 ; "........" + .byte $00 ; "." diff --git a/fendersdbl_bin.c b/fendersdbl_bin.c new file mode 100644 index 0000000..e3fc5cd --- /dev/null +++ b/fendersdbl_bin.c @@ -0,0 +1,86 @@ +/* C source created by blob2c from input file fendersdbl.bin */ + +unsigned char fendersdbl_bin[] = { + /* 0 */ 0x00,0x03,0x00,0x07,0x77,0xe4,0xa0,0x00, /* ....w... */ + /* 8 */ 0x8c,0x44,0x02,0xc8,0x84,0x09,0x8c,0x01, /* .D...... */ + /* 16 */ 0x03,0xce,0x06,0x03,0xa9,0x4e,0x8d,0x02, /* .....N.. */ + /* 24 */ 0x03,0xa9,0x40,0x8d,0x03,0x03,0xa9,0x0c, /* ..@..... */ + /* 32 */ 0x8d,0x08,0x03,0xa9,0x00,0x8d,0x09,0x03, /* ........ */ + /* 40 */ 0xa9,0x10,0x8d,0x04,0x03,0xa9,0x0b,0x8d, /* ........ */ + /* 48 */ 0x05,0x03,0x20,0x59,0xe4,0x30,0xdd,0xa9, /* .. Y.0.. */ + /* 56 */ 0x04,0x8d,0x15,0x0b,0xa9,0x01,0x8d,0x16, /* ........ */ + /* 64 */ 0x0b,0xa9,0x00,0x8d,0x17,0x0b,0xa9,0x4f, /* .......O */ + /* 72 */ 0x8d,0x02,0x03,0xa9,0x80,0x8d,0x03,0x03, /* ........ */ + /* 80 */ 0x20,0x59,0xe4,0x30,0xbf,0xa9,0x52,0x8d, /* Y.0..R. */ + /* 88 */ 0x02,0x03,0xa9,0x40,0x8d,0x03,0x03,0xa9, /* ...@.... */ + /* 96 */ 0x80,0x8d,0x04,0x03,0xa9,0x08,0x8d,0x05, /* ........ */ + /* 104 */ 0x03,0xa9,0x00,0x8d,0x08,0x03,0xa9,0x01, /* ........ */ + /* 112 */ 0x8d,0x09,0x03,0xa9,0xd0,0x8d,0x0a,0x03, /* ........ */ + /* 120 */ 0xa9,0x02,0x8d,0x0b,0x03,0x20,0x59,0xe4, /* ..... Y. */ + /* 128 */ 0x30,0xd3,0xad,0x30,0x02,0x85,0xb2,0xad, /* 0..0.... */ + /* 136 */ 0x31,0x02,0x85,0xb3,0xa9,0x00,0x8d,0x2f, /* 1....../ */ + /* 144 */ 0x02,0xa9,0x3a,0x8d,0x30,0x02,0xa9,0x09, /* ..:.0... */ + /* 152 */ 0x8d,0x31,0x02,0xa9,0x6c,0x85,0xb4,0xa9, /* .1..l... */ + /* 160 */ 0x09,0x85,0xb5,0xa9,0x69,0x85,0xb1,0xa5, /* ....i... */ + /* 168 */ 0xb1,0x8d,0x0a,0x03,0xa9,0x01,0x8d,0x0b, /* ........ */ + /* 176 */ 0x03,0x20,0xde,0x08,0xe6,0xb1,0xca,0xbd, /* . ...... */ + /* 184 */ 0x10,0x0b,0xf0,0x4f,0x30,0x41,0x29,0x01, /* ...O0A). */ + /* 192 */ 0xd0,0x3d,0xe6,0xb0,0xa4,0xb0,0xbd,0x13, /* .=...... */ + /* 200 */ 0x0b,0x99,0xc0,0x00,0xbd,0x14,0x0b,0x99, /* ........ */ + /* 208 */ 0xe0,0x00,0x98,0x18,0x69,0xa0,0xa0,0x03, /* ....i... */ + /* 216 */ 0x91,0xb4,0xc8,0xa9,0x8e,0x91,0xb4,0xc8, /* ........ */ + /* 224 */ 0xc8,0xbd,0x15,0x0b,0xe8,0x38,0xe9,0x20, /* .....8. */ + /* 232 */ 0x91,0xb4,0xc0,0x10,0xd0,0xf2,0x18,0xa5, /* ........ */ + /* 240 */ 0xb4,0x69,0x14,0x85,0xb4,0x90,0x02,0xe6, /* .i...... */ + /* 248 */ 0xb5,0xa5,0xb0,0xc9,0x14,0xf0,0x0c,0x8a, /* ........ */ + /* 256 */ 0x29,0xf0,0x18,0x69,0x10,0xaa,0x0a,0x90, /* )..i.... */ + /* 264 */ 0xae,0xb0,0x9c,0xa9,0x22,0x8d,0x2f,0x02, /* ...."./. */ + /* 272 */ 0xad,0x0b,0xd4,0xd0,0xfb,0x20,0x26,0x09, /* ..... &. */ + /* 280 */ 0x38,0xe9,0x40,0xc5,0xb0,0xf0,0x02,0xb0, /* 8.@..... */ + /* 288 */ 0xf4,0xaa,0xb5,0xc0,0x8d,0x0a,0x03,0xb5, /* ........ */ + /* 296 */ 0xe0,0x8d,0x0b,0x03,0xa9,0x68,0x85,0xb4, /* .....h.. */ + /* 304 */ 0xa9,0x09,0x85,0xb5,0xca,0xf0,0x0d,0x18, /* ........ */ + /* 312 */ 0xa5,0xb4,0x69,0x14,0x85,0xb4,0x90,0xf4, /* ..i..... */ + /* 320 */ 0xe6,0xb5,0xd0,0xf0,0xa0,0x00,0xb9,0x2f, /* ......./ */ + /* 328 */ 0x09,0x91,0x58,0xc8,0xc0,0x09,0xd0,0xf6, /* ..X..... */ + /* 336 */ 0xb1,0xb4,0x91,0x58,0xc8,0xc0,0x15,0xd0, /* ...X.... */ + /* 344 */ 0xf7,0xa9,0x00,0x8d,0x2f,0x02,0xa5,0xb2, /* ..../... */ + /* 352 */ 0x8d,0x30,0x02,0xa5,0xb3,0x8d,0x31,0x02, /* .0....1. */ + /* 360 */ 0xa9,0x22,0x8d,0x2f,0x02,0xad,0x0b,0xd4, /* ."./.... */ + /* 368 */ 0xd0,0xfb,0xa0,0x00,0x98,0x99,0x80,0x00, /* ........ */ + /* 376 */ 0xc8,0x10,0xfa,0x20,0xfc,0x08,0xca,0x20, /* ... ... */ + /* 384 */ 0xcc,0x08,0x85,0x43,0x20,0xcc,0x08,0x85, /* ...C ... */ + /* 392 */ 0x44,0x25,0x43,0xc9,0xff,0xf0,0xf0,0x20, /* D%C.... */ + /* 400 */ 0xcc,0x08,0x85,0x45,0x20,0xcc,0x08,0x85, /* ...E ... */ + /* 408 */ 0x46,0x20,0xcc,0x08,0x91,0x43,0xe6,0x43, /* F ...C.C */ + /* 416 */ 0xd0,0x04,0xe6,0x44,0xf0,0x0a,0xa5,0x45, /* ...D...E */ + /* 424 */ 0xc5,0x43,0xa5,0x46,0xe5,0x44,0xb0,0xe9, /* .C.F.D.. */ + /* 432 */ 0xad,0xe2,0x02,0x0d,0xe3,0x02,0xf0,0xc7, /* ........ */ + /* 440 */ 0x86,0x49,0x20,0xc9,0x08,0xa6,0x49,0xa0, /* .I ...I. */ + /* 448 */ 0x00,0x8c,0xe2,0x02,0x8c,0xe3,0x02,0xf0, /* ........ */ + /* 456 */ 0xb6,0x6c,0xe2,0x02,0xe0,0xfd,0xd0,0x51, /* .l.....Q */ + /* 464 */ 0xad,0x0a,0x03,0x0d,0x0b,0x03,0xd0,0x24, /* .......$ */ + /* 472 */ 0xad,0x2f,0x02,0x6c,0xe0,0x02,0xa9,0x31, /* ./.l...1 */ + /* 480 */ 0x8d,0x00,0x03,0xa9,0x52,0x8d,0x02,0x03, /* ....R... */ + /* 488 */ 0xa9,0x10,0x8d,0x04,0x03,0xa9,0x0b,0x8d, /* ........ */ + /* 496 */ 0x05,0x03,0xa9,0x00,0x8d,0x08,0x03,0xa9, /* ........ */ + /* 504 */ 0x01,0x8d,0x09,0x03,0xa9,0x40,0x8d,0x03, /* .....@.. */ + /* 512 */ 0x03,0x20,0x59,0xe4,0x30,0xf6,0xad,0x0d, /* . Y.0... */ + /* 520 */ 0x0c,0x29,0x03,0x8d,0x0b,0x03,0xad,0x0e, /* .)...... */ + /* 528 */ 0x0c,0x8d,0x0a,0x03,0xad,0x17,0xd0,0xad, /* ........ */ + /* 536 */ 0x0f,0x0c,0x8d,0xcd,0x08,0xa0,0x00,0xa2, /* ........ */ + /* 544 */ 0x00,0xbd,0x10,0x0b,0xe8,0x60,0xad,0x25, /* .....`.% */ + /* 552 */ 0xe4,0x48,0xad,0x24,0xe4,0x48,0x60,0x00, /* .H.$.H`. */ + /* 560 */ 0x00,0x2c,0x6f,0x61,0x64,0x69,0x6e,0x67, /* .,oading */ + /* 568 */ 0x00,0x00,0x70,0x70,0x70,0x47,0x58,0x09, /* ..pppGX. */ + /* 576 */ 0x70,0x06,0x06,0x06,0x06,0x06,0x06,0x06, /* p....... */ + /* 584 */ 0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06, /* ........ */ + /* 592 */ 0x06,0x06,0x06,0x06,0x06,0x41,0x3a,0x09, /* .....A:. */ + /* 600 */ 0x00,0x00,0x00,0x00,0x61,0x74,0x61,0x72, /* ....atar */ + /* 608 */ 0x69,0x00,0x61,0x72,0x63,0x61,0x64,0x65, /* i.arcade */ + /* 616 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* ........ */ + /* 624 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* ........ */ + /* 632 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 /* ........ */ +}; /* fendersdbl_bin */ + +int fendersdbl_bin_len = 640; diff --git a/fendersdbl_bin.h b/fendersdbl_bin.h new file mode 100644 index 0000000..b27302c --- /dev/null +++ b/fendersdbl_bin.h @@ -0,0 +1,9 @@ +/* C header created by blob2c from input file fendersdbl.bin */ + +#ifndef fendersdbl_bin_H +#define fendersdbl_bin_H + +extern unsigned char fendersdbl_bin[]; +extern int fendersdbl_bin_len; + +#endif /* fendersdbl_bin_H */ diff --git a/fendersdbl_offsets.h b/fendersdbl_offsets.h new file mode 100644 index 0000000..af60bcd --- /dev/null +++ b/fendersdbl_offsets.h @@ -0,0 +1,5 @@ +#define OFFSET_COLDST_1_DD 0x0007 +#define OFFSET_COLDST_2_DD 0x000b +#define OFFSET_ROTCOLOR_DD 0x0214 +#define OFFSET_SCREENOFF_DD 0x01d8 +#define OFFSET_TITLE_DD 0x0258 diff --git a/get_address.c b/get_address.c new file mode 100644 index 0000000..cc503ad --- /dev/null +++ b/get_address.c @@ -0,0 +1,22 @@ +#include <stdio.h> + +int get_address(char *self, char *arg) { + unsigned int got; + + if(sscanf(arg, "0x%x", &got) != 1) + if(sscanf(arg, "$%x", &got) != 1) + if(sscanf(arg, "%d", &got) != 1) { + fprintf(stderr, "Invalid address '%s'\n", arg); + return -1; + } + + if(got >= 0x10000) { + if(self) fprintf(stderr, "%s: ", self); + fprintf(stderr, "Address '%s' not in range $0000-$FFFF\n", arg); + return -1; + } + + return (int)got; +} + + diff --git a/get_address.h b/get_address.h new file mode 100644 index 0000000..27488fe --- /dev/null +++ b/get_address.h @@ -0,0 +1,2 @@ +int get_address(char *self, char *arg); + diff --git a/loadscreen.bin b/loadscreen.bin Binary files differnew file mode 100644 index 0000000..7ae3e00 --- /dev/null +++ b/loadscreen.bin diff --git a/loadscreen.dasm b/loadscreen.dasm new file mode 100644 index 0000000..e623d09 --- /dev/null +++ b/loadscreen.dasm @@ -0,0 +1,153 @@ + +; 20070524 bkw: DASM source for GR.2 "LOADING..." screen. +; Object code is intended to be prepended to some other object +; file, so it uses INITAD rather than RUNAD. + +; WARNING: if you modify this file such that the object code changes, +; make sure you edit cart2bin.c and correct the offset for the title +; screen text! As long as you don't change anything after the "loading" +; label, this won't be a problem. + + processor 6502 + + include "equates.inc" + + seg.u ZP + org $80 + + if * > $ff + echo "Zero page vars have overrun into the stack! * =", * + err + else + echo *-$80, "bytes of zero page used,", $ff-*, "remain" + endif + + seg CODE + +LOAD_ADDR = $6000 ; load at 24K +origin = LOAD_ADDR-6; + org origin ; make room for 6-byte header + +; 2-byte Atari executable header: + byte $FF, $FF ; let DOS know it's a binary load file + +; start of first segment + +; 4-byte segment header: + word LOAD_ADDR ; Load address + word endbin-1 ; Last byte to load + +; Rest of segment contains code and data +pagesize byte 0 ; cart2bin will fill this in with the size of + ; the cart image code, in pages. +main: + +; set RAMTOP to 28K, call GR.2, store "LOADING..." in screen RAM, +; then set RAMTOP back to original value. + + lda RAMTOP + cmp #$C0 ; do we have 48K? + bcs ram_ok + + ldx #0 ; no! print error message... + lda #9 + sta ICCOM + lda #<ram_msg + sta ICBAL + lda #>ram_msg + sta ICBAH + lda #ram_msg_len + sta ICBLL + stx ICBLH + jsr CIOV +hang bcs hang ; spin forever + +ram_msg byte "Need at least 48K to run this.", $9B +ram_msg_len = *-ram_msg+1 + +ram_ok + ;sec ; carry already set by cmp above! + sbc pagesize ; adjust RAMTOP to make room for the converted cart image + pha ; save old RAMTOP value on stack + + lda #$70 ; set RAMTOP to $7000 (28K). Later on we do GRAPHICS 2, + sta RAMTOP ; which will cause the DL and screen RAM to go here. + + ldx #$60 ; CLOSE #6 first + lda #12 ; Command 12=CLOSE + sta ICCOM,x + jsr CIOV ; call CIO, ignore any error + + ;ldx #$60 ; set up IOCB #6 for CIO GRAPHICS command + lda #3 ; Command 3=OPEN + sta ICCOM,x + lda #12 ; 12=R/W access + sta ICAX1,x + lda #2 ; GR. mode 2 + sta ICAX2,x + lda #<s_dev ; S: device + sta ICBAL,x + lda #>s_dev + sta ICBAH,x + lda #s_dev_len + sta ICBLL,x + lda #0 + sta ICBLH,x + jsr CIOV ; call CIO + + lda #5 ; POSITION 5,5 + sta ROWCRS + sta COLCRS + lda #0 + sta COLCRS+1 + + sta ICBLH,x ; PRINT #6;"LOADING... +; ldx #$60 + lda #9 ; Command 9 = CIO "put record" + sta ICCOM,x + lda #<loading + sta ICBAL,x + lda #>loading + sta ICBAH,x + lda #loading_len + sta ICBLL,x + jsr CIOV ; call CIO + +; ldx #$60 ; CLOSE #6 + lda #12 ; Command 12=CLOSE + sta ICCOM,x + jsr CIOV ; call CIO, ignore any error + + pla ; get adjusted RAMTOP value + sta RAMTOP + + rts ; end of init routine, return control to DOS + +s_dev byte "S:" +s_dev_len equ *-s_dev+1 + +; WARNING: if you change any code/data below this line, you'll probably need +; to change the offset in cart2bin.c as well. +loading byte " LOADING" + ds 6 +title ds 20 ; cart2bin will fill in these 20 bytes with the filename + byte $9B ; (or title, if -t is used) +loading_len equ *-loading+1 + +endbin ; end of first segment + + echo "Code is", *-origin+1, "bytes" + +; start of second segment (which loads at INITAD, to tell DOS +; where to start running the init code loaded in segment 1) + + ; 4-byte segment header: + word INITAD ; load address (loading into INITAD lets our code run) + word INITAD+1 ; Last byte to load + + ; Segment data contains 2 bytes (the actual run address) + word main ; the 2 bytes to stuff into RUNAD + + ; That's all, folks! + echo "Title offset: loadscreen_bin_len - ", *-title + diff --git a/loadscreen_bin.c b/loadscreen_bin.c new file mode 100644 index 0000000..5c4d9bd --- /dev/null +++ b/loadscreen_bin.c @@ -0,0 +1,34 @@ +/* C source created by blob2c from input file loadscreen.bin */ + +unsigned char loadscreen_bin[] = { + /* 0 */ 0xff,0xff,0x00,0x60,0xd1,0x60,0x00,0xa5, /* ...`.`.. */ + /* 8 */ 0x6a,0xc9,0xc0,0xb0,0x3d,0xa2,0x00,0xa9, /* j...=... */ + /* 16 */ 0x09,0x8d,0x42,0x03,0xa9,0x25,0x8d,0x44, /* ..B..%.D */ + /* 24 */ 0x03,0xa9,0x60,0x8d,0x45,0x03,0xa9,0x20, /* ..`.E.. */ + /* 32 */ 0x8d,0x48,0x03,0x8e,0x49,0x03,0x20,0x56, /* .H..I. V */ + /* 40 */ 0xe4,0xb0,0xfe,0x4e,0x65,0x65,0x64,0x20, /* ...Need */ + /* 48 */ 0x61,0x74,0x20,0x6c,0x65,0x61,0x73,0x74, /* at least */ + /* 56 */ 0x20,0x34,0x38,0x4b,0x20,0x74,0x6f,0x20, /* 48K to */ + /* 64 */ 0x72,0x75,0x6e,0x20,0x74,0x68,0x69,0x73, /* run this */ + /* 72 */ 0x2e,0x9b,0xed,0x00,0x60,0x48,0xa9,0x70, /* ....`H.p */ + /* 80 */ 0x85,0x6a,0xa2,0x60,0xa9,0x0c,0x9d,0x42, /* .j.`...B */ + /* 88 */ 0x03,0x20,0x56,0xe4,0xa9,0x03,0x9d,0x42, /* . V....B */ + /* 96 */ 0x03,0xa9,0x0c,0x9d,0x4a,0x03,0xa9,0x02, /* ....J... */ + /* 104 */ 0x9d,0x4b,0x03,0xa9,0xac,0x9d,0x44,0x03, /* .K....D. */ + /* 112 */ 0xa9,0x60,0x9d,0x45,0x03,0xa9,0x03,0x9d, /* .`.E.... */ + /* 120 */ 0x48,0x03,0xa9,0x00,0x9d,0x49,0x03,0x20, /* H....I. */ + /* 128 */ 0x56,0xe4,0xa9,0x05,0x85,0x54,0x85,0x55, /* V....T.U */ + /* 136 */ 0xa9,0x00,0x85,0x56,0x9d,0x49,0x03,0xa9, /* ...V.I.. */ + /* 144 */ 0x09,0x9d,0x42,0x03,0xa9,0xae,0x9d,0x44, /* ..B....D */ + /* 152 */ 0x03,0xa9,0x60,0x9d,0x45,0x03,0xa9,0x25, /* ..`.E..% */ + /* 160 */ 0x9d,0x48,0x03,0x20,0x56,0xe4,0xa9,0x0c, /* .H. V... */ + /* 168 */ 0x9d,0x42,0x03,0x20,0x56,0xe4,0x68,0x85, /* .B. V.h. */ + /* 176 */ 0x6a,0x60,0x53,0x3a,0x20,0x20,0x4c,0x4f, /* j`S: LO */ + /* 184 */ 0x41,0x44,0x49,0x4e,0x47,0x00,0x00,0x00, /* ADING... */ + /* 192 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* ........ */ + /* 200 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* ........ */ + /* 208 */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x9b, /* ........ */ + /* 216 */ 0xe2,0x02,0xe3,0x02,0x01,0x60 /* .....` */ +}; /* loadscreen_bin */ + +int loadscreen_bin_len = 222; diff --git a/loadscreen_bin.h b/loadscreen_bin.h new file mode 100644 index 0000000..80cf77f --- /dev/null +++ b/loadscreen_bin.h @@ -0,0 +1,9 @@ +/* C header created by blob2c from input file loadscreen.bin */ + +#ifndef loadscreen_bin_H +#define loadscreen_bin_H + +extern unsigned char loadscreen_bin[]; +extern int loadscreen_bin_len; + +#endif /* loadscreen_bin_H */ diff --git a/manftr.rst b/manftr.rst new file mode 100644 index 0000000..e26d352 --- /dev/null +++ b/manftr.rst @@ -0,0 +1,30 @@ +COPYRIGHT +========= + +WTFPL. See http://www.wtfpl.net/txt/copying/ for details. + +AUTHOR +====== + +B. Watson <urchlay@slackware.uk>; Urchlay on irc.libera.chat *##atari*. + +SEE ALSO +======== + +**a8eol**\(1), +**a8utf8**\(1), +**atr2xfd**\(1), +**atrsize**\(1), +**axe**\(1), +**blob2c**\(1), +**cart2xex**\(1), +**dasm2atasm**\(1), +**fenders**\(1), +**rom2cart**\(1), +**unmac65**\(1), +**xexcat**\(1), +**xexsplit**\(1), +**xfd2atr**\(1). + +Any good Atari 8-bit book: *De Re Atari*, *The Atari BASIC Reference +Manual*, the *OS Users' Guide*, *Mapping the Atari*, etc. diff --git a/manhdr.rst b/manhdr.rst new file mode 100644 index 0000000..bd5b0f1 --- /dev/null +++ b/manhdr.rst @@ -0,0 +1,7 @@ +.. include:: ver.rst +.. |date| date:: + +:Manual section: 1 +:Manual group: Urchlay's Atari 8-bit Tools +:Date: |date| +:Version: |version| diff --git a/rom2cart.1 b/rom2cart.1 new file mode 100644 index 0000000..d339b02 --- /dev/null +++ b/rom2cart.1 @@ -0,0 +1,244 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "ROM2CART" 1 "2022-08-29" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +rom2cart \- Convert a raw ROM image to an Atari800 CART image, or vice versa +.\" RST source for rom2cart(1) man page. Convert with: +. +.\" rst2man.py rom2cart.rst > rom2cart.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fBrom2cart\fP [\fI\-chlnrv\fP] [\-m \fImachine\fP] [\-t \fItype\fP] [\-T \fIforced\-type\fP] [\-C \fIforced\-checksum\fP] [\-U \fIunused\-data\fP] [\fIinfile\fP] [\fIoutfile\fP] +.sp +\fBcart2rom\fP [\fIinfile\fP] [\fIoutfile\fP] +.SH DESCRIPTION +.sp +\fBrom2cart\fP converts between raw ROM dumps and Atari800 \fB\&.CAR\fP +images. Despite the name, conversion can be done in either direction. +\fBcart2rom\fP is equivalent to \fBrom2cart \-r\fP\&. +.sp +Input ROM files may be either raw dumps or Atari800 .CAR format. +Output will be a \fB\&.CAR\fP file, or (with \fB\-r\fP) a raw dump. +.sp +When reading a raw dump, \fBrom2cart\fP attempts to determine +the correct image type based on the file size, machine type +(specified via \fB\-m\fP, or guessed by looking at the ROM +content), and (optional) user\-supplied type number or name (\fB\-t\fP). +If \fBrom2cart\fP is unable to narrow the selection down to one +image type, it will "guess" by choosing the lowest\-numbered type +that matches the given parameters (unless \fB\-n\fP is given to prevent +this behavior). +.sp +When writing a \fB\&.CAR\fP file, \fBrom2cart\fP will calculate the checksum +automatically and store it in the \fBCART\fP header (unless \fB\-C\fP is +used to force the checksum). +.SH OPTIONS +.sp +Standard options: +.INDENT 0.0 +.TP +.B \-c +Check ROM and print info only; do not create any output. +.TP +.B \-h +Print help (usage) message and exit. +.TP +.B \-l +Print a complete list of known image types and exit. +.TP +.BI \-m \ machine +Sets the machine type for the image. Valid machine values are +\fB5200\fP and \fB8bit\fP (may be abbreviated as \fB5\fP and \fB8\fP). This is used as +a hint by the type\-guessing algorithm to narrow down the search. +May be used in combination with \fB\-t\fP\&. Without \fB\-t\fP, only the machine +type and file size are used to guess the image type. This option +has no effect with \fB\-T\fP\&. +.TP +.B \-n +Do not guess image type, if unable to determine it exactly from the +file size and any supplied \fB\-t\fP/\fB\-T\fP/\fB\-m\fP arguments. +The type\-matching is still done, and if only one type matches, +it will be used. This option only has an effect if type\-matching +results in two or more possible matches. This option has no effect +with \fB\-T\fP\&. +.TP +.B \-r +Output a raw image dump, rather than a \fB\&.CAR\fP image. Most useful +when input is a \fB\&.CAR\fP image, but may be used with raw input (in +which case, the output file will be a copy of the input, but it +will only be created if \fBrom2cart\fP thinks the input is a valid raw +dump). If output filename not specified, it will be derived from +the input filename, and will end in \fI\&.rom\fP\&. +.TP +.BI \-t \ type +Set the image type. type may be either a valid numeric type or +a name. If a numeric type is given, it must be a valid type, the +file size must be correct for the type, and if given, the machine +type (\fB\-m\fP option) must match the type. If a name is given, it is used +to search the list of known types; only names that match will be +considered as possible types. This is a case\-insensitive substring +match. +.TP +.B \-v +Verbose operation. May be given twice, for extra verbosity. +.UNINDENT +.sp +Advanced options: +.sp +The "advanced" options are considered advanced because they\(aqre capable +of creating a bogus \fB\&.CAR\fP file that Atari800 won\(aqt accept. They\(aqre also +useful for debugging \fBrom2cart\fP or Atari800 itself. +.INDENT 0.0 +.TP +.BI \-T \ type +Force the image type. Unlike \fB\-t\fP, the \fItype\fP given must be numeric, +and is not required to be a known type. When using this option, +the file size is not checked. \fB\-T\fP is intended to be used for image +types that were not yet in existence when this version of \fBrom2cart\fP +was written. +.TP +.BI \-C \ sum +Force the checksum field in the output \fB\&.CAR\fP image to \fIsum\fP\&. +Intended for debugging purposes. Atari800 will refuse to load +\fB\&.CAR\fP images with invalid checksums. \fIsum\fP is a 32\-bit unsigned +value, and may be given in hex (prefixed with $ or 0x) or decimal +(no prefix). This option has no effect if the output is a raw dump +(\fB\-r\fP option). +.TP +.BI \-U \ data +Set the unused bytes (offsets 12\-15) in the \fBCART\fP header to \fIdata\fP\&. +Currently, these bytes are unused by Atari800, but future versions +may define a use for them. Normally, \fBrom2cart\fP sets them to all +zeroes. \fIdata\fP is a 32\-bit unsigned value, and may be given in hex +(prefixed with $ or 0x) or decimal (no prefix). This option has no +effect if the output is a raw dump (\fB\-r\fP option). +.UNINDENT +.SH NOTES +.sp +\fIinfile\fP may be \(aq\-\(aq to read from standard input. \fIoutfile\fP may be \(aq\-\(aq to +write to standard output. \fBrom2cart\fP will refuse to write binary data to +a terminal. +.sp +If \fIoutfile\fP is omitted, but \fIinfile\fP is provided, the output filename will +be constructed from \fIinfile\fP by replacing the filename extension with +\fI\&.car\fP (or \fI\&.rom\fP if \fB\-r\fP is given), or by appending \fI\&.car\fP (or \fI\&.rom\fP) if there +is no extension. +.sp +\fBrom2cart\fP contains an internal database of image types. The current +version uses the list from Atari800 v2.0.3 (types 1\-43). If you +need to create a \fB\&.CAR\fP image of a type not supported in this +version of \fBrom2cart\fP, you can use the \fB\-T\fP option. Alternatively, look +for a newer version of \fBrom2cart\fP\&. If no new version exists, bug the +author until he releases one! +.sp +The \fB\-T\fP option should be used with caution. It disables all the +checks that are normally done, and can be used to create \fB\&.CAR\fP files +with arbitrary types and data sizes (e.g. a 16K image with its +type set to "Standard 8K", or an image whose type isn\(aqt recognized +by Atari800 at all). With \fB\-T\fP, it\(aqs up to you to ensure that the image +type and size is correct. +.SH CART FORMAT +.sp +The \fB\&.CAR\fP format is fully documented in \fIcart.txt\fP, supplied +with the Atari800 source distribution. The following is an abbreviated +description. +.sp +A \fB\&.CAR\fP image consists of a 16\-byte header followed by the ROM data. +.sp +The first 4 bytes contain \(aqC\(aq \(aqA\(aq \(aqR\(aq \(aqT\(aq in ASCII. +.sp +The next 4 bytes contain the cartridge type in MSB (aka +\fIbig\-endian\fP) format. +.sp +The next 4 bytes contain cartridge checksum in MSB format (ROM only). +.sp +The next 4 bytes are currently unused (zero). +.sp +The rest of the file contains the ROM data: 4, 8, 16, 32, 40, 64, 128, +256, 512 or 1024 kilobytes. +.SH HEURISTICS +.sp +If none of the \fB\-m\fP, \fB\-n\fP, \fB\-T\fP options are given, the machine type is +guessed according to these rules: +.sp +First, examine the option byte (3rd\-to\-last in the ROM image). If +it\(aqs \fI$FF\fP or in the range \fI$50\-$59\fP, assume 5200. If it\(aqs \fI$04\fP, \fI$05\fP, or +\fI$80\fP, assume 8\-bit computer. +.sp +If the option byte doesn\(aqt help, and if the ROM is 32K or larger +in size, the cartridge init address (last two bytes of ROM) is +checked. If it falls in the range \fI$4000\-$7FFF\fP, it must be a 5200 ROM +(because cartridge ROM starts at \fI$8000\fP on the 8\-bit). +.sp +If the ROM is less than 32K, and/or its init address is +>=$8000, rom2cart searches the first 8K of ROM data, looking for +6502 machine code that writes to \fI$E8xx\fP (5200 POKEY) or \fI$D2xx\fP +(8\-bit POKEY). If there are 3 or more "5200 POKEY" writes and zero or +one "8\-bit POKEY) writes, assume 5200. If 3 or more "8\-bit POKEY" +writes and zero or one "5200 POKEY" writes, assume 8\-bit. +.sp +If the machine type is still unknown, \fBrom2cart\fP will choose +the lowest\-numbered cartridge type that matches the ROM size, +regardless of machine type. +.SH EXIT STATUS +.sp +Exit status is zero for success, non\-zero for failure. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/rom2cart.c b/rom2cart.c new file mode 100644 index 0000000..f6f8754 --- /dev/null +++ b/rom2cart.c @@ -0,0 +1,641 @@ +/* Convert a raw cart dump into a .CAR + + Either the user sets the type, in which case we make sure the file size + matches, or else he doesn't, and we try to guess from the file size. + Also give options to force the type in case our cart_types is out of + data, and to force the checksum for debugging purposes. + + Type guessing algorithm... we have some or all of these: + + File size - always have. Eliminate all types of any other size. + + Machine type - optional, via -m. If we have it, eliminate all types of + the wrong machine type. + + Type name partial string - optional, via -t. If we have it, eliminate + all non-matching types. + + Type number - optional, via -t. If we have it, eliminate all but the + given type number. + + If list is now empty, give error message. + If list is only one element long, use it as-is. + Otherwise... there are >1 elements in the list. + + If the user has disabled guessing, show the him list, ask him to be + more specific. + + (optional heuristics:) + - Look at the option byte (3rd to last byte of the ROM) + - If the list contains both machine types, try to narrow it down by + looking at the init address ($4000-7FFF is definitely 5200). + - Also try looking for 3 or more STA $E8xx instructions: on the 5200, + these are writes to POKEY; on the 8bit, they're writes to ROM (unlikely). + + If list is only one element long, use it as-is. + + If he hasn't disabled guessing, then guess! Take lowest-numbered type... +*/ + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> +#include <ctype.h> + +#include "cart.h" + +#ifndef VERSION +#define VERSION "???" +#endif + +#define SELF "rom2cart" + +#define BANNER \ + SELF " v" VERSION " by B. Watson (WTFPL)\n" + +char *usage = + "\nUsage: " SELF " [-chl] [-t type] [-T type] [-C sum] [infile] [outfile]\n" + " cart2rom [infile] [outfile] (equivalent to " SELF " -r)\n" + " -h Print this help.\n" + " -v Verbose operation (-vv for 'very verbose').\n" + " -c Check only; do not create output file (enables -vv).\n" + " -t type Set cartridge type. [type] may be a numeric type or a\n" + " partial name to match.\n" + " -l List all known cartridge types.\n" + " -m mach Cart is for given machine, either '8bit' or '5200'.\n" + " -n Do not guess cart type, if unable to narrow down to one.\n" + " -r Write a raw ROM image as output, rather than a CART image.\n" + " -T type Like -t, but forces the type. Numeric type only.\n" + " -C sum Force the checksum to sum. Not very useful.\n" + " -U data Set 'unused' bytes in CART header to data.\n" + "\n" + "infile may be either a raw dump or a CART image, and may be '-' to read\n" + "from standard input. outfile may be omitted, in which case output will\n" + "be written to a file named after infile, with its extension changed to\n" + ".car (or .rom, if -r given). outfile may be '-' to write to standard\n" + "output.\n"; + +#define OPTIONS "vchlm:nT:t:C:rU:" + +void print_type_table(int *set) { + /* print a nicely-formatted type table. If set is NULL, print all types, + otherwise only print types whose bit in set is true. */ + int i; + fprintf(stderr, "%5s | %8s | %6s | %s\n", + "Type", "Machine", "SizeKB", "Name"); + fprintf(stderr, "------|----------|--------|------------------------\n"); + for(i=1; i<=MAX_CART_TYPE; i++) { + if(set == NULL || set[i]) + fprintf(stderr, "%5d | %8s | %6d | %s\n", + i, + (cart_types[i].machine == M_5200 ? "5200" : "8-bit"), + cart_types[i].size, + cart_types[i].name); + } +} + +/* read_file() reads and mallocs this many bytes at a time. There should + never be a reason to change this. */ +#define READ_CHUNK_SIZE 4096 + +int read_file(FILE *f, unsigned char **bufp) { + /* read an entire file, malloc'ing as we go. caller must free(*bufp). + return value is number of bytes read. */ + int bytes_read = 0, total_read = 0; + + *bufp = malloc(READ_CHUNK_SIZE); + if(!*bufp) { + fclose(f); + fputs("Out of memory (initial malloc failed)\n", stderr); + exit(1); + } + + while((bytes_read = fread(*bufp + total_read, 1, READ_CHUNK_SIZE, f)) > 0) { + unsigned char *newbuf; + + total_read += bytes_read; + newbuf = realloc(*bufp, total_read + READ_CHUNK_SIZE); + + if(!newbuf) { + fclose(f); + free(*bufp); + fputs("Out of memory (realloc failed)\n", stderr); + exit(1); + } + + *bufp = newbuf; + } + + return total_read; +} + +/* case-insensitive strstr()-like search */ +char *string_match(char *haystack, char *needle) { + while(*haystack) { + char *htmp = haystack; + char *ntmp = needle; + + if(tolower(*ntmp) == tolower(*haystack)) { + while(tolower(*ntmp) == tolower(*htmp)) { + ntmp++; + htmp++; + + if(!*ntmp) + return haystack; + + if(!*htmp) + break; + } + } + + haystack++; + } + + return NULL; +} + +machine_t code_heuristics(unsigned char *rom, int size, int verbose) { + /* Look at the first 8K (minus 6-byte header), count possible + occurences of POKEY writes. The POKEY is at $E800 on the 5200 and + $D200 on the 8-bit. Return value is the (probable) machine type, + which may be M_INVALID if the test is inconclusive. */ + int i, a8 = 0, a52 = 0; + int machine = M_INVALID; + + for(i=0; i < (size < 8192 ? (size-6) : (8192-6) ); i++) { + unsigned char opcode = rom[i]; + unsigned char page = rom[i+2]; + + /* STA abs STX abs STA abs,X */ + if(opcode == 0x8d || opcode == 0x8e || opcode == 0x9d) { + if(page == 0xe8) + a52++; + else if(page == 0xd2) + a8++; + } + } + + if(verbose > 1) { + fprintf(stderr, + SELF ": found %d probable 8-bit POKEY writes\n", a8); + fprintf(stderr, + SELF ": found %d probable 5200 POKEY writes\n", a52); + } + + if(a8 >= 3 && a52 <= 1) + machine = M_ATARI8; + else if(a52 >= 3 && a8 <= 1) + machine = M_5200; + + return machine; +} + +machine_t guess_machine_type(unsigned char *rom, int size, int verbose) { + /* Try to guess the machine type based on the option byte and/or + init address. If that fails, call code_heuristics(). + Return value is the guessed machine type, which may be M_INVALID if + we can't figure it out. */ + machine_t machine = M_INVALID; + unsigned char option = rom[size - 3]; + int init = rom[size - 2] | (rom[size - 1] << 8); + + if(verbose) + fprintf(stderr, + SELF ": using heuristics to guess machine type " + "(-n or -m to inhibit)\n"); + + if(option == 0xff || (option >= 0x50 && option <= 0x59)) { + /* 0xff means diagnostic cart on the 5200. 0x50 thru 0x59 are the + digits 0-9 for non-diagnostic carts (the 2nd digit of the + copyright year) */ + machine = M_5200; + if(verbose > 1) + fprintf(stderr, SELF ": machine type is 5200 based on option byte " + "$%02X\n", option); + } else if(option == 4 || option == 5 || option == 0x80) { + /* These are the most common option bytes for A8. */ + machine = M_ATARI8; + if(verbose > 1) + fprintf(stderr, SELF ": machine type is 8-bit based on option byte " + "$%02X\n", option); + } else if(init >= 0x4000 && init < 0x8000 && size >= 32768) { + /* 32K and up 5200 carts may have an init address below $8000. On + the 8-bit, addresses below $8000 are RAM, not part of the cart + address window. */ + machine = M_5200; + if(verbose > 1) + fprintf(stderr, SELF ": machine type is 5200 based on init address " + "$%04X\n", init); + } else { + /* If all else fails, look for STA $E8xx (5200 POKEY writes), + or STA $D2xx (A8 POKEY writes). Of course, data tables might + happen to contain these sequences of bytes... */ + machine = code_heuristics(rom, size, verbose); + } + + if(verbose) { + if(machine == M_INVALID) + fprintf(stderr, SELF ": heuristics couldn't guess machine type\n"); + else + fprintf(stderr, SELF ": heuristics guessed machine type: %s\n", + (machine == M_ATARI8 ? "8-bit" : "5200")); + } + + return machine; +} + +int determine_type( + int verbose, + int allow_guess, + int type_override, + char *type_param, + machine_t machine, + int size, + unsigned char *rom) +{ + /* Figure out the cartridge type (ID), based on the image size, machine + type (optional), and/or some heuristics that look at the ROM itself. + Return value is the type, which may be 0 if we can't guess. + candidates[] has one "bit" (int) per cart type, and is initialized to + all zeroes. As we find possible type matches, we turn on the bit(s) in + candidates[] (or turn them off, as types are eliminated). + + All this ugly table-searching code would be much prettier as an + SQL select, but it'd be overkill to require a Real Database for a + small niche-purpose utility like this... + */ + int type = 0, i, match, both, candidates[MAX_CART_TYPE + 1]; + + /* process -t/-T first */ + if(type_param && *type_param >= '1' && *type_param <= '9') { + type = atoi(type_param); + if(type_override) { + if(type) + return type; /* valid numeric -T */ + else + return 0; /* bogus -T */ + } else { + if(!type || type > MAX_CART_TYPE) { + fprintf(stderr, "Invalid numeric type '%d' for -t (valid types " + "are 1 to %d; use -l for list)\n", type, MAX_CART_TYPE); + return 0; + } + } + } + + /* no -T, so do a search */ + for(i=0; i<=MAX_CART_TYPE; i++) + candidates[i] = 0; + + if(type) { + /* -t numeric type */ + if(size != (cart_types[type].size * 1024)) { + fprintf(stderr, SELF ": ROM size does not match, type %d " + "should be %dK, but input is %dK\n", + type, cart_types[type].size, size/1024); + return 0; + } + + candidates[type] = 1; + machine = cart_types[type].machine; /* bypass machine-type guessing */ + } else if(type_param) { + /* -t string match */ + match = 0; + if(verbose) + fprintf(stderr, SELF ": trying to match '%s'\n", type_param); + + for(i=1; i<=MAX_CART_TYPE; i++) { + if(string_match(cart_types[i].name, type_param)) { + if(verbose > 1) + fprintf(stderr, SELF ": matched type %d (%s)\n", + i, cart_types[i].name); + candidates[i] = 1; + match++; + } + } + + if(!match) { + fprintf(stderr, SELF ": no types match '%s'\n", type_param); + return 0; + } + } else { + /* no -t param at all, all types are potential matches */ + for(i=0; i<=MAX_CART_TYPE; i++) + candidates[i] = 1; + } + + /* now eliminate all wrong-sized types */ + match = 0; + for(i=1; i<=MAX_CART_TYPE; i++) { + if(cart_types[i].size * 1024 == size) + match++; + else + candidates[i] = 0; + } + + /* size doesn't match anything in the table! Whoops! */ + if(!match) { + fprintf(stderr, SELF ": no known types match size %.1fKB\n", + (double)size / 1024.0); + return 0; + } + + /* see if both machine types are in the list */ + match = both = 0; + if(machine == M_INVALID) { + for(i=1; i<=MAX_CART_TYPE; i++) { + if(!candidates[i]) + continue; + + machine_t mt = cart_types[i].machine; + if(match) { + if(mt != match) { + both = 1; + break; + } + } else { + match = mt; + } + } + + if(!both) machine = match; + } + + if((verbose > 1) && both) + fprintf(stderr, SELF ": candidate list contains both machine types\n"); + + /* if no machine type override, try some heuristics to determine the + machine type. */ + if(allow_guess && (machine == M_INVALID)) + machine = guess_machine_type(rom, size, verbose); + + /* if -m given or guessed, eliminate all types of wrong machine type */ + if(machine != M_INVALID) { + for(i=1; i<=MAX_CART_TYPE; i++) { + if(cart_types[i].machine != machine) + candidates[i] = 0; + } + } + + /* now see whether we have 0, 1, or >1 matches */ + type = match = 0; + for(i=1; i<=MAX_CART_TYPE; i++) { + if(candidates[i]) { + match++; + if(!type) type = i; /* take the lowest-numbered type that matches */ + } + } + + if(!match) { + /* d'oh! Nothing matched... */ + return 0; + } else if(match == 1) { + /* Exact match, use it */ + if(verbose) { + fprintf(stderr, SELF ": exact match:\n"); + print_type_table(candidates); + } + + return type; + } else { + /* >1 match... */ + if(allow_guess) { + if(verbose) { + fprintf(stderr, SELF ": %d types match:\n", match); + print_type_table(candidates); + } + fprintf(stderr, SELF ": guessing type %d (%s)\n", + type, cart_types[type].name); + return type; + } else { + fprintf(stderr, SELF ": %d types match, not guessing:\n", match); + print_type_table(candidates); + return 0; + } + } +} + +/* helper for -C and -U options */ +unsigned int get_u32_value(char opt, char *arg) { + unsigned int got; + + if(sscanf(arg, "0x%x", &got) != 1) + if(sscanf(arg, "$%x", &got) != 1) + if(sscanf(arg, "%u", &got) != 1) { + fprintf(stderr, "Invalid value for -%c: '%s'\n", opt, arg); + exit(1); + } + + return got; +} + +int main(int argc, char **argv) { + char *infile = "-", outfile[4096] = "-"; + FILE *in = stdin, *out = stdout; + unsigned char *rom; + char *type_param = NULL; + int check_only = 0, type = 0, type_override = 0, allow_guess = 1, + verbose = 0, raw_output = 0; + machine_t machine = M_INVALID; + int c, size; + unsigned int cksum = 0, cksum_override = 0, unused_data = 0; + unsigned char *buffer; + unsigned char **bufp = &buffer; + unsigned char cart[16]; + + if(strstr(argv[0], "cart2rom") != NULL) { + raw_output = 1; + } + + while( (c = getopt(argc, argv, OPTIONS)) != -1 ) { + switch(c) { + case 'v': + verbose++; + break; + + case 'c': + check_only++; + verbose = 2; + break; + + case 'h': + fputs(BANNER, stderr); + fprintf(stderr, usage); + exit(0); + break; + + case 'l': + print_type_table(NULL); + exit(0); + break; + + case 'T': + type_override++; + /* fall through */ + + case 't': + type_param = optarg; + break; + + case 'C': + cksum = get_u32_value('C', optarg); + cksum_override++; + break; + + case 'U': + unused_data = get_u32_value('U', optarg); + break; + + case 'n': + allow_guess = 0; + break; + + case 'm': + if(optarg[0] == '5') + machine = M_5200; + else if(optarg[0] == 'a' || optarg[0] == '8') + machine = M_ATARI8; + else { + fprintf(stderr, SELF ": Invalid machine type (-m)\n" + "Valid types are '8bit' and '5200' (or '8' and '5')\n"); + exit(1); + } + break; + + case 'r': + raw_output++; + break; + + default: + fputs(BANNER, stderr); + fprintf(stderr, usage); + exit(1); + break; + } + } + + if(verbose) fputs(BANNER, stderr); + + if(optind < argc) { + infile = argv[optind]; + if(strcmp(infile, "-") == 0) { + in = stdin; + if(verbose) fprintf(stderr, SELF ": reading from standard input\n"); + } else { + char *p; + strcpy(outfile, infile); + p = strrchr(outfile, '.'); + if(!p) p = outfile + strlen(outfile); + *p = '\0'; + if(raw_output) + strcat(outfile, ".rom"); + else + strcat(outfile, ".car"); + + if( !(in = fopen(infile, "rb")) ) { + if(verbose) + fprintf(stderr, SELF ": (fatal) %s: %s\n", + infile, strerror(errno)); + exit(1); + } + } + optind++; + } + + size = read_file(in, bufp); + fclose(in); + + if(has_cart_signature(buffer)) { + rom = buffer + 16; + size -= 16; + type = get_cart_type(buffer); + if(verbose) fprintf(stderr, SELF ": input is CART image, type %d\n", type); + } else { + rom = buffer; + if(verbose) fprintf(stderr, SELF ": input is raw dump, %d bytes\n", size); + } + + if(size <= 0) { + fprintf(stderr, SELF ": input file is empty!\n"); + exit(1); + } else if(size % 4096) { + fprintf(stderr, SELF ": %s: input size not a multiple of 4KB!\n", + (type_override ? "warning" : "fatal")); + if(!type_override) exit(1); + } + + if(!type) { + type = determine_type( + verbose, allow_guess, type_override, type_param, machine, size, rom); + } + + if(!type) { + fprintf(stderr, SELF ": " + "Can't determine image type from supplied info and ROM size.\n" + "Please re-run with -t, -T, and/or -m options.\n"); + free(buffer); + exit(1); + } + + /* Built cart header. Don't use create_cart_header() because it assumes + the ROM size always matches the expected ROM size for the type. This + may NOT be the case, if someone uses -T to e.g. set the type to 16K + when there's only 8K of actual ROM. */ + memcpy(cart, CART_SIGNATURE, 4); + set_cart_type(cart, type); + if(!cksum_override) + cksum = calc_rom_checksum(buffer, size); + set_cart_checksum(cart, cksum); + set_cart_unused(cart, unused_data); + + if(verbose > 1) cart_dump_header(cart, 0); + + if(!check_only) { + if(optind < argc) { + strcpy(outfile, argv[optind]); + optind++; + } + + if(strcmp(outfile, "-") == 0) { + out = stdout; + if(verbose) fprintf(stderr, SELF ": writing to standard output\n"); + } else { + if(strcmp(infile, outfile) == 0) { + fprintf(stderr, SELF ": Input and output filenames are the same, " + "aborting!\n"); + exit(1); + } + if(verbose) fprintf(stderr, SELF ": output file is '%s'\n", outfile); + + if( !(out = fopen(outfile, "wb")) ) { + fprintf(stderr, SELF ": (fatal) %s: %s\n", outfile, strerror(errno)); + exit(1); + } + } + + if(isatty(fileno(out))) { + fprintf(stderr, SELF ": Standard output is a terminal, " + "not writing binary data.\n" + "Either redirect to a file or set the output filename.\n"); + exit(1); + } + } + + if(optind < argc) { + fprintf(stderr, SELF ": ignoring trailing junk on command line: " + "'%s' ...\n", + argv[optind]); + } + + if(!check_only) { + if(!raw_output) fwrite(cart, 16, 1, out); + fwrite(rom, size, 1, out); + fclose(out); + } + + free(buffer); + + exit(0); +} diff --git a/rom2cart.rst b/rom2cart.rst new file mode 100644 index 0000000..0edc57d --- /dev/null +++ b/rom2cart.rst @@ -0,0 +1,202 @@ +.. RST source for rom2cart(1) man page. Convert with: +.. rst2man.py rom2cart.rst > rom2cart.1 +.. rst2man.py comes from the SBo development/docutils package. + +======== +rom2cart +======== + +---------------------------------------------------------------- +Convert a raw ROM image to an Atari800 CART image, or vice versa +---------------------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +**rom2cart** [*-chlnrv*] [-m *machine*] [-t *type*] [-T *forced-type*] [-C *forced-checksum*] [-U *unused-data*] [*infile*] [*outfile*] + +**cart2rom** [*infile*] [*outfile*] + +DESCRIPTION +=========== + +**rom2cart** converts between raw ROM dumps and Atari800 **.CAR** +images. Despite the name, conversion can be done in either direction. +**cart2rom** is equivalent to **rom2cart -r**. + +Input ROM files may be either raw dumps or Atari800 .CAR format. +Output will be a **.CAR** file, or (with **-r**) a raw dump. + +When reading a raw dump, **rom2cart** attempts to determine +the correct image type based on the file size, machine type +(specified via **-m**, or guessed by looking at the ROM +content), and (optional) user-supplied type number or name (**-t**). +If **rom2cart** is unable to narrow the selection down to one +image type, it will "guess" by choosing the lowest-numbered type +that matches the given parameters (unless **-n** is given to prevent +this behavior). + +When writing a **.CAR** file, **rom2cart** will calculate the checksum +automatically and store it in the **CART** header (unless **-C** is +used to force the checksum). + +OPTIONS +======= + +Standard options: + +-c + Check ROM and print info only; do not create any output. + +-h + Print help (usage) message and exit. + +-l + Print a complete list of known image types and exit. + +-m machine + Sets the machine type for the image. Valid machine values are + **5200** and **8bit** (may be abbreviated as **5** and **8**). This is used as + a hint by the type-guessing algorithm to narrow down the search. + May be used in combination with **-t**. Without **-t**, only the machine + type and file size are used to guess the image type. This option + has no effect with **-T**. + +-n + Do not guess image type, if unable to determine it exactly from the + file size and any supplied **-t**/**-T**/**-m** arguments. + The type-matching is still done, and if only one type matches, + it will be used. This option only has an effect if type-matching + results in two or more possible matches. This option has no effect + with **-T**. + +-r + Output a raw image dump, rather than a **.CAR** image. Most useful + when input is a **.CAR** image, but may be used with raw input (in + which case, the output file will be a copy of the input, but it + will only be created if **rom2cart** thinks the input is a valid raw + dump). If output filename not specified, it will be derived from + the input filename, and will end in *.rom*. + +-t type + Set the image type. type may be either a valid numeric type or + a name. If a numeric type is given, it must be a valid type, the + file size must be correct for the type, and if given, the machine + type (**-m** option) must match the type. If a name is given, it is used + to search the list of known types; only names that match will be + considered as possible types. This is a case-insensitive substring + match. + +-v + Verbose operation. May be given twice, for extra verbosity. + +Advanced options: + +The "advanced" options are considered advanced because they're capable +of creating a bogus **.CAR** file that Atari800 won't accept. They're also +useful for debugging **rom2cart** or Atari800 itself. + +-T type + Force the image type. Unlike **-t**, the *type* given must be numeric, + and is not required to be a known type. When using this option, + the file size is not checked. **-T** is intended to be used for image + types that were not yet in existence when this version of **rom2cart** + was written. + +-C sum + Force the checksum field in the output **.CAR** image to *sum*. + Intended for debugging purposes. Atari800 will refuse to load + **.CAR** images with invalid checksums. *sum* is a 32-bit unsigned + value, and may be given in hex (prefixed with $ or 0x) or decimal + (no prefix). This option has no effect if the output is a raw dump + (**-r** option). + +-U data + Set the unused bytes (offsets 12-15) in the **CART** header to *data*. + Currently, these bytes are unused by Atari800, but future versions + may define a use for them. Normally, **rom2cart** sets them to all + zeroes. *data* is a 32-bit unsigned value, and may be given in hex + (prefixed with $ or 0x) or decimal (no prefix). This option has no + effect if the output is a raw dump (**-r** option). + +NOTES +===== + +*infile* may be '-' to read from standard input. *outfile* may be '-' to +write to standard output. **rom2cart** will refuse to write binary data to +a terminal. + +If *outfile* is omitted, but *infile* is provided, the output filename will +be constructed from *infile* by replacing the filename extension with +*.car* (or *.rom* if **-r** is given), or by appending *.car* (or *.rom*) if there +is no extension. + +**rom2cart** contains an internal database of image types. The current +version uses the list from Atari800 v2.0.3 (types 1-43). If you +need to create a **.CAR** image of a type not supported in this +version of **rom2cart**, you can use the **-T** option. Alternatively, look +for a newer version of **rom2cart**. If no new version exists, bug the +author until he releases one! + +The **-T** option should be used with caution. It disables all the +checks that are normally done, and can be used to create **.CAR** files +with arbitrary types and data sizes (e.g. a 16K image with its +type set to "Standard 8K", or an image whose type isn't recognized +by Atari800 at all). With **-T**, it's up to you to ensure that the image +type and size is correct. + +CART FORMAT +=========== + +The **.CAR** format is fully documented in *cart.txt*, supplied +with the Atari800 source distribution. The following is an abbreviated +description. + +A **.CAR** image consists of a 16-byte header followed by the ROM data. + +The first 4 bytes contain 'C' 'A' 'R' 'T' in ASCII. + +The next 4 bytes contain the cartridge type in MSB (aka +*big-endian*) format. + +The next 4 bytes contain cartridge checksum in MSB format (ROM only). + +The next 4 bytes are currently unused (zero). + +The rest of the file contains the ROM data: 4, 8, 16, 32, 40, 64, 128, +256, 512 or 1024 kilobytes. + +HEURISTICS +========== + +If none of the **-m**, **-n**, **-T** options are given, the machine type is +guessed according to these rules: + +First, examine the option byte (3rd-to-last in the ROM image). If +it's *$FF* or in the range *$50-$59*, assume 5200. If it's *$04*, *$05*, or +*$80*, assume 8-bit computer. + +If the option byte doesn't help, and if the ROM is 32K or larger +in size, the cartridge init address (last two bytes of ROM) is +checked. If it falls in the range *$4000-$7FFF*, it must be a 5200 ROM +(because cartridge ROM starts at *$8000* on the 8-bit). + +If the ROM is less than 32K, and/or its init address is +>=$8000, rom2cart searches the first 8K of ROM data, looking for +6502 machine code that writes to *$E8xx* (5200 POKEY) or *$D2xx* +(8-bit POKEY). If there are 3 or more "5200 POKEY" writes and zero or +one "8-bit POKEY) writes, assume 5200. If 3 or more "8-bit POKEY" +writes and zero or one "5200 POKEY" writes, assume 8-bit. + +If the machine type is still unknown, **rom2cart** will choose +the lowest-numbered cartridge type that matches the ROM size, +regardless of machine type. + +EXIT STATUS +=========== + +Exit status is zero for success, non-zero for failure. + +.. include:: manftr.rst diff --git a/rstman.rst b/rstman.rst new file mode 100644 index 0000000..361ade5 --- /dev/null +++ b/rstman.rst @@ -0,0 +1,58 @@ +.. RST source for PRGNAM(1) man page. Convert with: +.. rst2man.py PRGNAM.rst > PRGNAM.1 +.. rst2man.py comes from the SBo development/docutils package. + +====== +PRGNAM +====== + +------------------------------- +description of whatever this is +------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +*PRGNAM* [*-options*] + +DESCRIPTION +=========== + +Description of PRGNAM + +OPTIONS +======= + +--version Output version number + +-?, -h, --help + Output usage string + +**-foo** *BAR* + Use *BAR* for fooing. + +**-baz** + Stuff with baz. + +-v Verbose mode + +.. other sections we might want, uncomment as needed. + +.. FILES +.. ===== + +.. ENVIRONMENT +.. =========== + +.. EXIT STATUS +.. =========== + +.. BUGS +.. ==== + +.. EXAMPLES +.. ======== + +.. include:: manftr.rst @@ -0,0 +1,2 @@ +Writing this file to a disk image should result in only one sector being used in the image, because I fixed the bug in axe.. +Writing this file to a disk image should result in only one sector being used in the image, because I fixed the bug in axe.. diff --git a/testdata b/testdata new file mode 100644 index 0000000..54729a2 --- /dev/null +++ b/testdata @@ -0,0 +1 @@ +Writing this file to a disk image should result in only one sector being used in the image, because I fixed the bug in axe.. diff --git a/testdata.orig b/testdata.orig new file mode 100644 index 0000000..54729a2 --- /dev/null +++ b/testdata.orig @@ -0,0 +1 @@ +Writing this file to a disk image should result in only one sector being used in the image, because I fixed the bug in axe.. diff --git a/unmac65.1 b/unmac65.1 new file mode 100644 index 0000000..71c29c9 --- /dev/null +++ b/unmac65.1 @@ -0,0 +1,392 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "UNMAC65" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +unmac65 \- Detokenize Atari 8-bit Mac/65 SAVEd files. +.\" RST source for unmac65(1) man page. Convert with: +. +.\" rst2man.py unmac65.rst > unmac65.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fBunmac65\fP [\fI\-options\fP] \fIfile.m65\fP +.SH DESCRIPTION +.sp +\fBunmac65\fP reads files created with Mac/65\(aqs SAVE command (usually called +\&.M65 files) and converts them back to plain text assembly source. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-a +Use ATASCII EOLs. This option is not available in Atari version, +since it already uses ATASCII. +.TP +.B \-c +Convert non\-printable characters constants to hex bytes. +.UNINDENT +.INDENT 0.0 +.TP +.B \fB\-cc\fP +Convert all character constants to hex bytes. +.TP +.B \-e nnn[,iii] +Renumber the program, starting at line \fInnn\fP, with increment \fIiii\fP\&. +\fInnn\fP must be present, and must be an integer greater than or equal +to zero. \fIiii\fP is optional, must be positive (non\-zero), and +defaults to 10 if not given. +.sp +Mac/65\(aqs maximum line number is 65535. unmac65 will happily renumber +lines with no upper bound (other than unsigned int overflow), so pay +attention. +.UNINDENT +.INDENT 0.0 +.TP +.B \-h +Show command\-line help. +.TP +.B \-i +Convert inverse video (in comments and strings) to standard ASCII. +Lines that were converted will get a comment "\fI; XXX inverse\fP" at +the end. This option also enables the \-c option. +.sp +If the program contained any inverse\-video strings, the resulting +output will \fInot\fP reassemble correctly; you\(aqll have to edit it to +e.g. change the formerly inverse video strings to a list of hex +bytes. +.sp +This option is not available in the Atari version. +.TP +.B \-l +Lowercase mnemonics and hex constants (but not labels or comments). +.UNINDENT +.INDENT 0.0 +.TP +.B \fB\-la\fP +Lowercase mnemonics, hex constants, labels, and comments (but not +strings or character constants). +.UNINDENT +.INDENT 0.0 +.TP +.B \-n +No line numbers in output. +.TP +.BI \-o \ file +Output to \fIfile\fP (default = standard output). +.TP +.B \-q +Add closing single\-quote to character constants. Changes this: +.INDENT 7.0 +.INDENT 3.5 +.sp +.nf +.ft C +LDA #\(aqA +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +\&...to this: +.INDENT 7.0 +.INDENT 3.5 +.sp +.nf +.ft C +LDA #\(aqA\(aq +.ft P +.fi +.UNINDENT +.UNINDENT +.TP +.B \-p +Omit leading . (period) from pseudo\-ops (e.g. print BYTE for .BYTE). +.TP +.B \-t +Replace leading spaces with tabs. +.UNINDENT +.INDENT 0.0 +.TP +.B \fB\-ta\fP +Replace spaces between all fields with tabs. +.UNINDENT +.INDENT 0.0 +.TP +.B \-v +Verbose output (dump tokens in hex). Useful for examining damaged +\&.M65 files, or debugging unmac65 itself. +.UNINDENT +.SS Human\-readable Output Options +.sp +The \-m, \-r, and \-u options are not available for the Atari, and may or +may not be useful on non\-Linux OSes. +.INDENT 0.0 +.TP +.B \-m +Print inverse video as pseudo\-underlined, using backspace and +underscore. Useful for piping to more(1) or less(1). Can be combined +with \-u. +.TP +.B \-r +Print inverse video as reverse video using xterm/ANSI compatible +escape sequences. Can be combined with \-u. Useful for piping to +less(1) provided its \-r or \-R option is used. +.TP +.B \-u +Print ATASCII control characters as their nearest Unicode +equivalents (encoded in UTF\-8). Depending on your terminal, +combining this option with \-r may not work properly. Also, depending +on the font(s) your terminal is using, you may see boxes instead of +control characters. If this happens, try a different font, or a +different terminal (the author recommends rxvt\-unicode). +.UNINDENT +.sp +Options may not be bundled (use "\-p \-t", not "\-pt"). +.sp +Unlike most UNIX\-flavored programs, the CLI options are +case\-insensitive. This is to make life easier for users of the Atari +version, where uppercase is the normal way of doing things. +.sp +The \-c, \-cc, \-l, \-la, \-n, \-p, \-t, \-ta options are provided to assist in +porting Mac/65 programs to other assemblers, such as ca65 or dasm. +unmac65\(aqs output with none of these options (or with \-n only) is +acceptable as input for the atasm assembler. This is true even if there +are inverse video strings: they look funny when viewing the file, but +atasm handles them correctly. +.sp +The \-v option prints the hex bytes for each line (preceded by ";;" and +the line number) after that line\(aqs detokenized listing. +.sp +Note that the output from \-m, \-r, \-u is intended for humans to read. +They\(aqre not very useful if you\(aqre trying to port Mac/65 code to a +different assembler, as none of them know what to do with the +underlines, ANSI codes, or pseudo\-ATASCII Unicode characters. +.SH FILE FORMAT +.sp +A tokenized Mac/65 file consists of: +.sp +Header: 2 byte $FE $FE signature, followed by the 2 byte program length +in LSB/MSB format. Length doesn\(aqt include the 4 header bytes. +.sp +The rest of the file consists of lines of code. Each line is: +.sp +Line number, 2 bytes (LSB/MSB format) +.sp +Line length, 1 byte. Total length, including the line number and length +bytes. +.sp +Tokens. Length minus 3 bytes of tokens. If the line is labelled, the +label will appear first, as a tokenized string (see below). +.sp +Whether or not there\(aqs a label, the next byte is the token for a +mnemonic (or pseudo\-op). Lines containing only a comment will have a +special token meaning "no mnemonic". +.sp +After the mnemonic token, 0 or more bytes of operands. Quoted strings or +labels as operands are stored as a tokenized string. Hex or decimal +constants are preceded by a token indicating the length (one or two +bytes) and the type (hex or decimal). +.sp +If there is a comment, the last byte of the operand field will be an +ASCII semicolon. The remaining bytes on the line are the comment in +ASCII form. +.sp +Tokenized strings can occur in the label or operand parts of the line. +The first byte is the length of the string in bytes, with the high bit +set (e.g. $84 for a 4\-byte string), followed by that number of ASCII +bytes. The length doesn\(aqt include the first byte, so e.g. the string +"ABC" is stored as $83, $41, $42, $43. Mac/65 doesn\(aqt allow empty +strings, so a zero\-length string (length byte $80) is an error. +.sp +There are separate sets of tokens for mnemonics/pseudo\-ops and operands. +Mnemonic/pseudo\-op tokens run from 0 to $5F, and operand tokens run from +0 to $4D (with 0\-$09 being "special", and a few invalid tokens in the +range $0A\-$4D). See the C source for the full list (or a hex/ascii dump +of the Mac/65 ROM, which is where I got the lists to put them in the C +source). Also, you can run unmac65 with the \-v option to get a +line\-by\-line hex dump of the tokens. +.SH DIAGNOSTICS +.sp +unmac65: line XX contains NN non\-printable ATASCII characters <= $1F +.sp +Self\-explanatory. Depending on what you\(aqre going to use the converted +file for, this may or may not be a problem. Non\-fatal. This warning +doesn\(aqt occur in the Atari version of unmac65. Also, it doesn\(aqt occur if +the \-u option is in use. +.sp +unmac65: line XX contains NN inverse ATASCII characters >= $80 +.sp +Self\-explanatory. Depending on what you\(aqre going to use the converted +file for, this may or may not be a problem. Non\-fatal. Use the \-i option +to convert inverse video to normal. This warning doesn\(aqt occur in the +Atari version of unmac65. Also, it doesn\(aqt occur if any of the \-i, \-m, +or \-r options are in use. +.sp +unmac65: not a mac/65 file (missing $FEFE header) +.sp +Self\-explanatory. Fatal error. +.sp +unmac65: corrupt or truncated file? +.sp +The length of the last line in the file is longer that the number of +bytes remaining in the file (according to the file header). Fatal error. +.sp +unmac65: unexpected EOF? +.sp +The file is shorter than the file header\(aqs program length. Probably the +file is truncated; less probably, the length header got scrambled +somehow. Fatal error. +.sp +unmac65: file is too short (N bytes) +.sp +The minimum length for a Mac/65 file is 4 bytes (which would be an empty +program containing no lines). The input file was shorter than 4 bytes. +Fatal error. (Actually, Mac/65 will never produce a 4\-byte file, it\(aqs +just the theoretical minimum) +.sp +unmac65: file is valid but contains no lines of code +.sp +Self\-explanatory. Mac/65 creates a file like this if the SAVE command is +given before entering any code (at startup or after a NEW). The SAVEd +file will be 5 bytes in length, and utterly useless. Non\-fatal warning. +.sp +unmac65: line #lll <= prev line #mmm +.sp +The line numbers in the file are supposed to be stored in ascending +order. Somehow this file has the line numbers out of order. Non\-fatal, +but probably the rest of the file will be garbage. +.sp +unmac65: internal error, state n +.sp +This is a "this should never happen" error. It indicates a bug in the +program. If you ever see this error, please notify the author, and send +a copy of the Mac/65 program that caused it. This is a non\-fatal error, +but the output might be garbage. +.sp +[$nn?] or <$nn?> in the output (where nn is 2 hex digits) +.sp +These indicate unknown/invalid tokens. Either the file is damaged, or +there is a bug in the program. These are non\-fatal errors. If you ever +see them, please contact the author, and send a copy of the Mac/65 +program that caused them. +.sp +unmac65: ignoring extra junk at EOF +.sp +The file contains more bytes than the program length header says it +should. This usually means the file was stored on an old DOS disk or +transferred with a broken XMODEM implementation, and was padded to the +sector/block size. Alternately, the header bytes got corrupted somehow +(this is highly unlikely, especially if there are no other +errors/warnings). Non\-fatal error. +.sp +Other errors are possible (e.g. disk full, I/O error reading input), but +they\(aqre not specific to unmac65; no need to list them all there. +.sp +Fatal errors result in unmac65 terminating. A non\-fatal error can +usually be recovered from, though the line that caused it will probably +be printed strangely. +.sp +It\(aqs probably worth mentioning also that Mac/65 source files can contain +ATASCII graphics or escape codes, although it\(aqs not a very common +practice. If you see strange stuff and/or your terminal misbehaves when +writing to standard output, try writing to a file (\-o option) instead. +See also the \-m, \-r, \-u options for human\-readable output. The Atari +version will render ATASCII graphics just fine, of course. +.SH EXIT STATUS +.sp +unmac65 will normally exit with a zero (success) status upon completion. +A non\-zero status indicates a fatal error. +.SH LIMITATIONS +.sp +The main difference between Mac/65\(aqs LIST output and unmac65\(aqs output is +that Mac/65 lines up the label, mnemonic, and comment fields (if the +field contents are short enough to fit in the allotted width), while +unmac65 makes no attempt to do so. If the field alignment is important +to you, try the \-t or \-ta options (which insert hard tabs, unlike +mac65\(aqs spaces). A future version of unmac65 may correct this minor +flaw, but as it stands, I\(aqve tested quite a few .M65 files by running +them through unmac65, then ENTERing unmac65\(aqs listed file in Mac/65 and +reSAVEing them... In all cases, the newly created tokenized files +compare identically to the original .M65 file. If you come across a file +that doesn\(aqt do this, yet is valid (can be assembled without error by +Mac/65), please send it to me, so I can fix unmac65! +.sp +Although a few helpful options have been added for porting Mac/65 +sources to other assemblers, unmac65 doesn\(aqt completely automate the +process. Depending on the assembler you\(aqre using, you may still have a +lot of manual edits to make. A future version of unmac65 may add a few +more options, but some hypothetical complex porting functions (like +"convert to ca65 format") would require implementing a much more complex +parser (such as a yacc\-based recursive descent parser). Most likely this +will never happen, if only because I want the program to be usable on +the Atari itself, with its limited memory and C compiler support. +.sp +There are a few ways that an invalid file can sneak past unmac65\(aqs error +checking. The same files wouldn\(aqt load correctly in Mac/65 either, but +generally don\(aqt cause any errors in Mac/65 (just silent failure). It\(aqd +be nice if unmac65 could act as a "M65 lint" for Mac/65 users. +.sp +unmac65 is mostly developed/tested against the OSS Mac/65 v1.01 +cartridge. The various disk versions appear to use the same tokenized +format, but haven\(aqt been well tested. If you have problems, please +contact the author. +.sp +(A further limitation is that the documentation isn\(aqt very concise. +Sorry about that.) +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/unmac65.c b/unmac65.c new file mode 100644 index 0000000..42875bb --- /dev/null +++ b/unmac65.c @@ -0,0 +1,1041 @@ +#include <stdio.h> +#include <unistd.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> + +#ifndef VERSION +# define VERSION "???" +#endif + +#ifndef TAG +# define TAG "" +#endif + +#define SELF "unmac65" +#define BANNER SELF " v" VERSION " by B. Watson (WTFPL)\n\n" + +#ifdef __CC65__ +# ifdef __ATARI__ +# define ATARI8 +# else +# error "This program only supports Atari 8-bit when built with cc65" +/* Feel free to add support for other systems, if you need to. The main + differences will be in fix_filename() and atari8_get_opts(). */ +# endif +#else +# undef ATARI8 +#endif + +#define lsbmsb(lo, hi) ( (lo) | (hi << 8) ) + +/* use static filename buffers on A8, since we (probably) won't be + getting them passed with argv. FIXME: I really ought to do bounds + checking... */ +#ifdef ATARI8 +char infile[128], outfile[128]; +#else +char *infile = NULL, *outfile = NULL; +#endif + +FILE *input = NULL, *output = NULL; +int using_stdout = 1; + +/* see handle_cli_opts() and/or atari8_get_opts() for these: */ +char nl = '\n'; +char no_numbers = 0; +char leading_tabs = 0; +char all_tabs = 0; +char omit_dots = 0; +char lcase_opcodes = 0; +char lcase_all = 0; +char dump_tokens = 0; +char add_quote = 0; + +unsigned int renum_start, renum_line; +int renum_incr = 0; + +typedef enum { CC_NONE, CC_UNPRINT, CC_ALL } chconst_opt_t; +chconst_opt_t chconsts_hex = 0; + +/* options only available in the non-Atari8 ports */ +#ifndef ATARI8 +char deinverse = 0; +char found_inverse = 0; +char found_unprint = 0; +char inv_underscore = 0; +char inv_ansi = 0; +char unicode = 0; +#endif + +/* dumpbuf[] really should be local to parse_one_line(), but + cc65 won't let us make this a local var, it's too big. 1000 bytes + is plenty (max line length is 256 bytes, we dump them in hex at + 3 chars each, plus 10-12 chars worth of formatting) */ +char dumpbuf[1000]; + +/* number of bytes left to read (initialized from 4-byte m65 header, + decremented by next_byte()). If this ever reaches 0 while there's + more input, or if we get EOF while prog_bytes != 0, it's an error. */ +unsigned int prog_bytes; + +/* Guess what these are for? */ +int line_number, old_line_number = -1; + +char *opcode_tokens[] = { + "ERROR -", /* 0x00 */ + ".IF", + ".ELSE", + ".ENDIF", + ".MACRO", + ".ENDM", + ".TITLE", + " ", + ".PAGE", + ".WORD", + ".ERROR", + ".BYTE", + ".SBYTE", + ".DBYTE", + ".END", + ".OPT", + ".TAB", /* 0x10 */ + ".INCLUDE", + ".DS", + ".ORG", + ".EQU", + "BRA", + "TRB", + "TSB", + ".FLOAT", + ".CBYTE", + ";", + ".LOCAL", + ".SET", + "*=", + "=", + ".=", + "JSR", /* 0x20 */ + "JMP", + "DEC", + "INC", + "LDX", + "LDY", + "STX", + "STY", + "CPX", + "CPY", + "BIT", + "BRK", + "CLC", + "CLD", + "CLI", + "CLV", + "DEX", /* 0x30 */ + "DEY", + "INX", + "INY", + "NOP", + "PHA", + "PHP", + "PLA", + "PLP", + "RTI", + "RTS", + "SEC", + "SED", + "SEI", + "TAX", + "TAY", + "TSX", /* 0x40 */ + "TXA", + "TXS", + "TYA", + "BCC", + "BCS", + "BEQ", + "BMI", + "BNE", + "BPL", + "BVC", + "BVS", + "ORA", + "AND", + "EOR", + "ADC", + "STA", /* 0x50 */ + "LDA", + "CMP", + "SBC", + "ASL", + "ROL", + "LSR", + "ROR", + "", /* 0x58 - the null opcode */ + "STZ", + "DEA", + "INA", + "PHX", + "PHY", + "PLX", + "PLY" /* 0x5f */ +}; + +/* Special opcodes */ +#define MAX_OPCODE 0x5f +#define NO_OPCODE 0x58 + +char *operand_tokens[] = { + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, /* actually "'", special handling tho */ /* 0x0a, 10 decimal */ + "%$", + "%", + "*", + " ", + " ", + "a", /* 0x10 */ + "q", + "+", + "-", + "*", /* 0x14, 20 decimal */ + "/", + "&", + ".DEF", + "=", + "<=", + ">=", + "<>", + ">", + "<", + "-", /* 0x1e, 30 dec */ + "[", + "]", /* 0x20 */ + ".OR", + ".AND", + ".NOT", + "!", + "^", + ".REF", + "\\", + NULL, /* 0x28, 40 dec */ + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + ".REF", + ".DEF ", /* 0x30 */ + ".NOT ", + " .AND ", /* 0x32, 50 dec */ + " .OR ", + " <", + " >", + ",X)", + "),Y", + ",Y", + ",X", + ")", + ",", /* 0x3b, 59, the null operand */ + "\x1b", /* 0x3c, 60, ASCII escape, chr$(27)? */ + ",", + "#", + "A", + "(", /* 0x40, 64 dec */ + "\"", + "$", + "Q", + "NO", + "NO ", + "OBJ", /* 0x46, 70 dec */ + "ERR", + "EJECT", + "LIST", + "XREF", + "MLIST", + "CLIST", + "NUM", /* 0x4d, 77 dec */ + /* "M", */ /* maybe? I think not... */ +}; + +/* Special operands */ +#define MAX_OPERAND 0x4d +#define NO_OPERAND 0x3b + +#define HEXWORD_PREFIX 5 +#define HEXBYTE_PREFIX 6 + +#define DECWORD_PREFIX 7 +#define DECBYTE_PREFIX 8 + +#define CHAR_CONST_PREFIX 0x0a + +/* Functions */ +void print_label_byte(unsigned char byte, FILE *output) { + putc((lcase_all ? tolower(byte) : byte), output); +} + +#ifdef ATARI8 +void print_string_byte(unsigned char byte, FILE *output) { + putc(byte, output); +} +#else +char *unicode_table[] = { + "♥", "┣", "┃", "┛", "┫", "┓", "╱", "╲", "◢", "▗", "◣", "▝", "▘", "▔", "▁", "▖", + "♣", "┏", "━", "╋", "⚫ ", "▄", "▎", "┳", "┻", "▌", "┗", "␛", "↑", "↓", "←", "→", + " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", + "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", + "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", + "◆", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", + "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "♠", "|", "↰", "◀", "▶" +}; + +/* This is probably more complex than it needs to be. */ +void start_inverse(FILE *output) { + if(inv_underscore) { + fputs("_\x08", output); + } else if(inv_ansi) { + fputs("\x1b[7m", output); + } +} + +void end_inverse(FILE *output) { + if(inv_underscore) { + /* do nothing */; + } else if(inv_ansi) { + fputs("\x1b[0m", output); + } +} + +void print_string_byte(unsigned char byte, FILE *output) { + int inverse; + + inverse = (byte >= 0x80); + byte &= 0x7f; + if(inverse && deinverse) { + found_inverse++; + inverse = 0; + } + + if(inverse) start_inverse(output); + if(unicode) { + fputs(unicode_table[byte], output); + } else { + if(inv_underscore || inv_ansi) { + putc(byte, output); + } else { + putc(inverse ? (byte | 0x80) : byte, output); + found_inverse += inverse; + } + if(byte < 0x20) found_unprint++; + } + if(inverse) end_inverse(output); +} + +#endif + +#define print_comment_byte(x, y) print_string_byte((lcase_all ? tolower(x) : x), y) + +void print_hex_byte(unsigned char byte, FILE *output) { + fprintf(output, (lcase_opcodes ? "$%02x" : "$%02X"), byte); +} + +void print_hex_word(int word, FILE *output) { + fprintf(output, (lcase_opcodes ? "$%04x" : "$%04X"), word); +} + + +void closeall() { + if(output && !using_stdout) { + fclose(output); + output = NULL; + } + + if(input) { + fclose(input); + input = NULL; + } +} + +void exit_cleanly(int status) { +#ifdef ATARI8 + if(status) { + puts("Press Return to exit:"); + fflush(stdout); + fgets(infile, 80, stdin); + } +#endif + + closeall(); + exit(status); +} + +unsigned char next_byte() { + int c; + + if(prog_bytes <= 0) { + fprintf(stderr, SELF ": corrupt or truncated file?\n"); + exit_cleanly(1); + } + + --prog_bytes; + c = getc(input); + if(c == EOF && prog_bytes != 0) { + fprintf(stderr, SELF ": unexpected EOF\n"); + exit_cleanly(1); + } + + if(prog_bytes == 0 && getc(input) != EOF) { + fprintf(stderr, SELF ": ignoring extra junk at EOF\n"); + } + + return (unsigned char)c; +} + +void parse_header(void) { + unsigned char inbuf[4]; + int bytes = fread(inbuf, 1, 4, input); + if(bytes < 4) { + if(ferror(input)) { + perror(infile); + } else { + fprintf(stderr, SELF ": file is too short (%d bytes)\n", bytes); + } + exit_cleanly(1); + } + + if(inbuf[0] != 0xfe || inbuf[1] != 0xfe) { + fprintf(stderr, SELF ": not a mac/65 file (missing $FEFE header)\n"); + exit_cleanly(1); + } + + prog_bytes = lsbmsb(inbuf[2], inbuf[3]); + if(!prog_bytes) fprintf(stderr, SELF ": file is valid but contains no lines of code\n"); + + if(dump_tokens) { + fprintf(output, ";; Mac/65 header: "); + for(bytes = 0; bytes < 4; bytes++) + fprintf(output, "%02X ", inbuf[bytes]); + fprintf(output, "length %d\n", prog_bytes); + } +} + +void print_opcode(unsigned char byte) { + char *opc = opcode_tokens[byte]; + + putc((leading_tabs ? '\t' : ' '), output); + if(omit_dots && opc[0] == '.' && isalpha(opc[1])) + ++opc; + + if(lcase_opcodes) { + char buf[10]; + char *p = buf; + strcpy(buf, opc); + while(*p) { + *p = tolower(*p); + ++p; + } + opc = buf; + } + + fputs(opc, output); + putc((all_tabs ? '\t' : ' '), output); +} + +/* Parser states. The parser is hand-rolled and kind of ugly. */ +#define ST_NEED_OPCODE 1 +#define ST_IN_LABEL 2 +#define ST_IN_OPERAND 3 +#define ST_IN_OPSTRING 4 +#define ST_IN_COMMENT 5 +#define ST_ERROR 6 +#define ST_IN_HEXBYTE 7 +#define ST_IN_HEXWORD_LSB 8 +#define ST_IN_HEXWORD_MSB 9 +#define ST_IN_DECBYTE 10 +#define ST_IN_DECWORD_LSB 11 +#define ST_IN_DECWORD_MSB 12 +#define ST_IN_CHAR_CONST 13 + +int handle_operand(unsigned char byte) { + char *operand = NULL; + + if(byte > 0x80) + return ST_IN_OPSTRING; + + if(byte == NO_OPERAND) { + putc((all_tabs ? '\t' : ' '), output); + return ST_IN_COMMENT; + } + + if(byte > MAX_OPERAND) + return ST_ERROR; + + operand = operand_tokens[byte]; + if(operand) { + fputs(operand, output); + return ST_IN_OPERAND; + } + + switch(byte) { + case HEXWORD_PREFIX: + return ST_IN_HEXWORD_LSB; + + case HEXBYTE_PREFIX: + return ST_IN_HEXBYTE; + + case DECWORD_PREFIX: + return ST_IN_DECWORD_LSB; + + case DECBYTE_PREFIX: + return ST_IN_DECBYTE; + + case CHAR_CONST_PREFIX: + return ST_IN_CHAR_CONST; + + /* TODO: find out if any other specials exist */ + + default: + fprintf(output, "[$%02X?]", byte); + return ST_IN_OPERAND; + } +} + +void parse_one_line() { + int state = ST_NEED_OPCODE; + int string_len = 0; + int line_bytes; + int lsb = 0; + unsigned char byte, linenum_lo, linenum_hi; + char hexbuf[20]; + +#ifndef ATARI8 + found_inverse = found_unprint = 0; +#endif + + linenum_lo = next_byte(); + linenum_hi = next_byte(); + + if(renum_incr) { + line_number = renum_line; + renum_line += renum_incr; + } else { + line_number = lsbmsb(linenum_lo, linenum_hi); + } + + if(line_number <= old_line_number) { + fprintf(stderr, SELF ": line #%d <= prev line #%d\n", + line_number, old_line_number); + } + + line_bytes = next_byte() - 3; + + if(dump_tokens) + sprintf(dumpbuf, ";; %d (%02X %02X, len %02X):", + line_number, linenum_lo, linenum_hi, line_bytes + 3); + + if(!no_numbers) { + char *format = "%06d "; + + /* duplicate mac65's weird line number formatting */ + if(line_number < 100) + format = "%02d "; + else if(line_number < 10000) + format = "%04d "; + + fprintf(output, format, line_number); + } + + while(line_bytes) { + byte = next_byte(); + --line_bytes; + + if(dump_tokens) { + sprintf(hexbuf, " %02X", byte); + strcat(dumpbuf, hexbuf); + } + + switch(state) { + case ST_NEED_OPCODE: + if(byte > 0x80) { + string_len = byte & 0x7f; + state = ST_IN_LABEL; + } else if(byte == NO_OPCODE) { + state = ST_IN_COMMENT; + } else if(byte <= MAX_OPCODE) { + print_opcode(byte); + state = ST_IN_OPERAND; + } else { + state = ST_ERROR; + } + break; + + case ST_IN_LABEL: + print_label_byte(byte, output); + if(--string_len == 0) + state = ST_NEED_OPCODE; /* TODO: 2 labels is error, detect */ + break; + + case ST_IN_OPERAND: + state = handle_operand(byte); + if(state == ST_IN_OPSTRING) string_len = byte & 0x7f; + break; + + case ST_IN_OPSTRING: + print_string_byte(byte, output); + if(--string_len == 0) + state = ST_IN_OPERAND; + break; + + case ST_IN_COMMENT: + print_comment_byte(byte, output); + break; + + case ST_ERROR: + fprintf(output, "<$%02X?>", byte); + break; + + case ST_IN_HEXBYTE: + print_hex_byte(byte, output); + state = ST_IN_OPERAND; + break; + + case ST_IN_HEXWORD_LSB: + lsb = byte; + state = ST_IN_HEXWORD_MSB; + break; + + case ST_IN_HEXWORD_MSB: + print_hex_word(lsbmsb(lsb, byte), output); + state = ST_IN_OPERAND; + break; + + case ST_IN_DECBYTE: + fprintf(output, "%d", byte); + state = ST_IN_OPERAND; + break; + + case ST_IN_DECWORD_LSB: + lsb = byte; + state = ST_IN_DECWORD_MSB; + break; + + case ST_IN_DECWORD_MSB: + fprintf(output, "%d", lsbmsb(lsb, byte)); + state = ST_IN_OPERAND; + break; + + case ST_IN_CHAR_CONST: +#ifndef ATARI8 + if( + chconsts_hex == CC_ALL || + (chconsts_hex == CC_UNPRINT && (byte < 0x20 || byte > 0x7f)) + ) + { + print_hex_byte(byte, output); + state = ST_IN_OPERAND; + break; + } +#endif + putc('\'', output); + print_string_byte(byte, output); + if(add_quote) + putc('\'', output); + state = ST_IN_OPERAND; + break; + + default: + fprintf(stderr, SELF ": internal error, state %d\n", state); + state = ST_ERROR; + break; + } + } + +#ifndef ATARI8 + if(found_inverse) { + fprintf(stderr, SELF ": line %d contains %d inverse ATASCII characters >= $80\n", line_number, found_inverse); + if(deinverse) printf("; XXX inverse (%d chars)", found_inverse); + } + if(found_unprint) fprintf(stderr, SELF ": line %d contains %d non-printable ATASCII characters <= $1F\n", line_number, found_unprint); +#endif + +#ifdef CYGWIN_NEWLINE_HACK + if(nl == '\n') putc('\r', output); +#endif + putc(nl, output); + + if(dump_tokens) { + fputs(dumpbuf, output); +#ifdef CYGWIN_NEWLINE_HACK + if(nl == '\n') putc('\r', output); +#endif + putc(nl, output); + } + + if(ferror(output)) { + perror(SELF); + exit_cleanly(1); + } +} + +void parse_lines() { + old_line_number = -1; + + while(prog_bytes) + parse_one_line(); + + closeall(); +} + +#ifdef ATARI8 +/* get rid of trailing newline, make uppercase... */ +void cleanstring(char *s) { + while(*s) { + *s = toupper(*s); + if(*s == '\n') *s = 0; + ++s; + } +} + +/* add .M65 if no extension entered, add leading D: if + no device name. */ +void fix_filename(char *src, char *dst, char *ext) { + if(!strrchr(src, '.')) + strcat(src, ext); + + dst[0] = '\0'; + + if(!strrchr(src, ':')) + strcat(dst, "D:"); + + strcat(dst, src); +} + +void prompt_for_opt(char *prompt, char *opt) { + char buffer[10]; + + printf("%s [%c/%c]?", + prompt, + (*opt ? 'Y' : 'y'), + (*opt ? 'n' : 'N')); + + fflush(stdout); + fgets(buffer, 10, stdin); + cleanstring(buffer); + + if(buffer[0] == 'Y' || buffer[0] == 'y') + *opt = 1; + else if(buffer[0] == 'N' || buffer[0] == 'n') + *opt = 0; + /* else leave unchanged */ +} + +void prompt_for_str(char *prompt, char *buf) { + fputs(prompt, stdout); + putc('?', stdout); + fflush(stdout); + fgets(buf, 120, stdin); + cleanstring(buf); +} + +void atari8_get_opts() { + char buffer[128]; + char other = 0; + + prompt_for_str("M65 file or Return to quit", buffer); + + if(!buffer[0]) { + prompt_for_opt("Really quit", &other); + if(other) + exit_cleanly(0); + else + other = 0; + } + + fix_filename(buffer, infile, ".M65"); + + prompt_for_str("Output file (Return for E:)", buffer); + + if(!buffer[0]) { + using_stdout = 1; + } else { + using_stdout = 0; + fix_filename(buffer, outfile, ".TXT"); + } + + prompt_for_opt("Set other options", &other); + if(other) { + prompt_for_opt("Omit line numbers", &no_numbers); + if(!no_numbers) { + prompt_for_opt("Renumber lines", &other); + if(other) { + prompt_for_str("Starting line number", buffer); + renum_start = atoi(buffer); + prompt_for_str("Line num increment (Return = 10)", buffer); + renum_incr = atoi(buffer); + if(renum_incr < 1) renum_incr = 10; + } else { + renum_incr = 0; + } + } + prompt_for_opt("Omit . (dots) from pseudo-ops", &omit_dots); + + prompt_for_opt("Lowercase everything", &lcase_all); + if(!lcase_all) + prompt_for_opt("Lowercase mnemonics, hex", &lcase_opcodes); + + prompt_for_opt("Replace leading spaces w/tabs", &leading_tabs); + if(!leading_tabs) + prompt_for_opt("Replace lead+inner spaces w/tabs", &all_tabs); + + other = (chconsts_hex == CC_UNPRINT); + prompt_for_opt("Unprintable char consts to hex", &other); + if(other) { + chconsts_hex = CC_UNPRINT; + } else { + other = (chconsts_hex == CC_ALL); + prompt_for_opt("All char consts to hex", &other); + if(other) + chconsts_hex = CC_ALL; + else + chconsts_hex = CC_NONE; + } + + if(chconsts_hex != CC_ALL) + prompt_for_opt("Close quote ' for char consts", &add_quote); + + prompt_for_opt("Dump tokens in hex", &dump_tokens); + } + + fflush(stdout); +} +#endif + +void usage() { + fprintf(stderr, "usage: " SELF " [options] inputfile\n\n"); + fprintf(stderr, "options:\n"); +#ifndef ATARI8 + fprintf(stderr, " -a Use ATASCII EOLs\n"); + fprintf(stderr, " -c Convert non-printable char constants to hex\n"); + fprintf(stderr, "-cc Convert all char constants to hex\n"); +#endif + fprintf(stderr, " -e nnn[,i] Renumber starting with nnn [increment i]\n"); + fprintf(stderr, " -h Help (this text)\n"); +#ifndef ATARI8 + fprintf(stderr, " -i Convert inverse video to normal\n"); +#endif + fprintf(stderr, " -l Lowercase mnemonics, hex constants\n"); + fprintf(stderr, "-la Lowercase all, including comments\n"); + fprintf(stderr, " -n No line numbers in output\n"); + fprintf(stderr, " -o [file] Output to file (default = stdout)\n"); + fprintf(stderr, " -p Omit leading . (period) from pseudo-ops\n"); + fprintf(stderr, " -q Add closing quote (') to character constants\n"); + fprintf(stderr, " -t Replace leading spaces with tabs\n"); + fprintf(stderr, "-ta Replace spaces between all fields with tabs\n"); + fprintf(stderr, " -v Verbose output (dump tokens)\n"); +#ifndef ATARI8 + fprintf(stderr, " -m Print inverse video as underlined\n"); + fprintf(stderr, " -r Print inverse video as ANSI reverse video\n"); + fprintf(stderr, " -u Print ATASCII as Unicode/UTF-8\n"); +#endif + exit(1); +} + +void get_renum_args(char *arg) { + renum_incr = 10; + /* atoi() doesn't detect errors, so: */ + if(!arg || arg[0] > '9' || arg[0] < '0') + usage(); + renum_start = atoi(arg); + arg = strchr(arg, ','); + if(arg) renum_incr = atoi(++arg); + if(!renum_incr || renum_incr < 0) usage(); +} + +/* TODO: support a few more -options + A lot of the fancier options I wanted to add, would require + a full parser for the grammar. I've avoided this partly because + it's more work, and partly because I dunno how well yacc/bison would + play with cc65... +*/ +void handle_cli_opts(int argc, char **argv) { +#ifdef ATARI8 + infile[0] = '\0'; +#endif + + while(++argv, --argc) { + if(argv[0][0] == '-') { + switch(tolower(argv[0][1])) { +#ifndef ATARI8 + case 'a': + nl = 0x9b; + if(argv[0][2]) usage(); + break; + + case 'i': + chconsts_hex = CC_UNPRINT; + deinverse = 1; + if(argv[0][2]) usage(); + break; + + case 'm': + inv_underscore = 1; + if(argv[0][2]) usage(); + break; + + case 'r': + inv_ansi = 1; + if(argv[0][2]) usage(); + break; + + case 'u': + unicode = 1; + if(argv[0][2]) usage(); + break; +#endif + case 'c': + chconsts_hex = CC_UNPRINT; + if(argv[0][2] == 'C' || argv[0][2] == 'c') + chconsts_hex = CC_ALL; + else if(argv[0][2]) usage(); + break; + + case 'e': + if(argv[0][2]) usage(); + if(!argv[1]) usage(); + get_renum_args(argv[1]); + argv++, argc--; + break; + + case 'n': + no_numbers = 1; + if(argv[0][2]) usage(); + break; + + case 'h': + usage(); + break; + + case 'l': + lcase_opcodes = 1; + if(argv[0][2] == 'A' || argv[0][2] == 'a') + lcase_all = 1; + else if(argv[0][2]) usage(); + break; + + case 'p': + omit_dots = 1; + if(argv[0][2]) usage(); + break; + + case 'q': + add_quote = 1; + if(argv[0][2]) usage(); + break; + + case 't': + leading_tabs = 1; + if(argv[0][2] == 'A' || argv[0][2] == 'a') + all_tabs = 1; + else if(argv[0][2]) usage(); + break; + + case 'v': + dump_tokens = 1; + if(argv[0][2]) usage(); + break; + + case 'o': + if(argv[0][2]) { +#ifdef ATARI8 + strcpy(outfile, &argv[0][2]); +#else + outfile = &argv[0][2]; +#endif + } else if(argc == 1) { + usage(); + } else { + ++argv, --argc; +#ifdef ATARI8 + strcpy(outfile, argv[0]); +#else + outfile = argv[0]; +#endif + } + using_stdout = 0; + break; + + default: + usage(); + break; + } + } else { +#ifdef ATARI8 + if(infile[0]) + usage(); + else + strcpy(infile, argv[0]); +#else + if(infile) + usage(); + else + infile = argv[0]; +#endif + } + } + + if(!infile) usage(); +} + +int main(int argc, char **argv) { + fputs(BANNER, stderr); + +#ifdef ATARI8 + while(1) { + if(argc < 2) { + atari8_get_opts(); + } else { + handle_cli_opts(argc, argv); + argc = 1; + } +#else + handle_cli_opts(argc, argv); +#endif + + input = fopen(infile, "rb"); + if(!input) { + perror(infile); + exit_cleanly(1); + } + + if(using_stdout) { + output = stdout; + } else { + output = fopen(outfile, "w"); + if(!output) { + perror(outfile); + exit_cleanly(1); + } + } + + if(renum_incr) renum_line = renum_start; + parse_header(); + parse_lines(); + +#ifdef ATARI8 + } +#endif + + exit_cleanly(0); + return 0; /* to shut gcc up... */ +} diff --git a/unmac65.rst b/unmac65.rst new file mode 100644 index 0000000..5330828 --- /dev/null +++ b/unmac65.rst @@ -0,0 +1,324 @@ +.. RST source for unmac65(1) man page. Convert with: +.. rst2man.py unmac65.rst > unmac65.1 +.. rst2man.py comes from the SBo development/docutils package. + +======= +unmac65 +======= + +------------------------------------------ +Detokenize Atari 8-bit Mac/65 SAVEd files. +------------------------------------------ + +.. include:: manhdr.rst + +SYNOPSIS +======== + +**unmac65** [*-options*] *file.m65* + +DESCRIPTION +=========== + +**unmac65** reads files created with Mac/65's SAVE command (usually called +.M65 files) and converts them back to plain text assembly source. + +OPTIONS +======= + +-a + Use ATASCII EOLs. This option is not available in Atari version, + since it already uses ATASCII. + +-c + Convert non-printable characters constants to hex bytes. + +**-cc** + Convert all character constants to hex bytes. + +-e nnn[,iii] + Renumber the program, starting at line *nnn*, with increment *iii*. + *nnn* must be present, and must be an integer greater than or equal + to zero. *iii* is optional, must be positive (non-zero), and + defaults to 10 if not given. + + Mac/65's maximum line number is 65535. unmac65 will happily renumber + lines with no upper bound (other than unsigned int overflow), so pay + attention. + +-h + Show command-line help. + +-i + Convert inverse video (in comments and strings) to standard ASCII. + Lines that were converted will get a comment "*; XXX inverse*" at + the end. This option also enables the -c option. + + If the program contained any inverse-video strings, the resulting + output will *not* reassemble correctly; you'll have to edit it to + e.g. change the formerly inverse video strings to a list of hex + bytes. + + This option is not available in the Atari version. + +-l + Lowercase mnemonics and hex constants (but not labels or comments). + +**-la** + Lowercase mnemonics, hex constants, labels, and comments (but not + strings or character constants). + +-n + No line numbers in output. + +-o file + Output to *file* (default = standard output). + +-q + Add closing single-quote to character constants. Changes this:: + + LDA #'A + + ...to this:: + + LDA #'A' + +-p + Omit leading . (period) from pseudo-ops (e.g. print BYTE for .BYTE). + +-t + Replace leading spaces with tabs. + +**-ta** + Replace spaces between all fields with tabs. + +-v + Verbose output (dump tokens in hex). Useful for examining damaged + .M65 files, or debugging unmac65 itself. + +Human-readable Output Options +----------------------------- + +The -m, -r, and -u options are not available for the Atari, and may or +may not be useful on non-Linux OSes. + +-m + Print inverse video as pseudo-underlined, using backspace and + underscore. Useful for piping to more(1) or less(1). Can be combined + with -u. + +-r + Print inverse video as reverse video using xterm/ANSI compatible + escape sequences. Can be combined with -u. Useful for piping to + less(1) provided its -r or -R option is used. + +-u + Print ATASCII control characters as their nearest Unicode + equivalents (encoded in UTF-8). Depending on your terminal, + combining this option with -r may not work properly. Also, depending + on the font(s) your terminal is using, you may see boxes instead of + control characters. If this happens, try a different font, or a + different terminal (the author recommends rxvt-unicode). + +Options may not be bundled (use "-p -t", not "-pt"). + +Unlike most UNIX-flavored programs, the CLI options are +case-insensitive. This is to make life easier for users of the Atari +version, where uppercase is the normal way of doing things. + +The -c, -cc, -l, -la, -n, -p, -t, -ta options are provided to assist in +porting Mac/65 programs to other assemblers, such as ca65 or dasm. +unmac65's output with none of these options (or with -n only) is +acceptable as input for the atasm assembler. This is true even if there +are inverse video strings: they look funny when viewing the file, but +atasm handles them correctly. + +The -v option prints the hex bytes for each line (preceded by ";;" and +the line number) after that line's detokenized listing. + +Note that the output from -m, -r, -u is intended for humans to read. +They're not very useful if you're trying to port Mac/65 code to a +different assembler, as none of them know what to do with the +underlines, ANSI codes, or pseudo-ATASCII Unicode characters. + +FILE FORMAT +=========== + +A tokenized Mac/65 file consists of: + +Header: 2 byte $FE $FE signature, followed by the 2 byte program length +in LSB/MSB format. Length doesn't include the 4 header bytes. + +The rest of the file consists of lines of code. Each line is: + +Line number, 2 bytes (LSB/MSB format) + +Line length, 1 byte. Total length, including the line number and length +bytes. + +Tokens. Length minus 3 bytes of tokens. If the line is labelled, the +label will appear first, as a tokenized string (see below). + +Whether or not there's a label, the next byte is the token for a +mnemonic (or pseudo-op). Lines containing only a comment will have a +special token meaning "no mnemonic". + +After the mnemonic token, 0 or more bytes of operands. Quoted strings or +labels as operands are stored as a tokenized string. Hex or decimal +constants are preceded by a token indicating the length (one or two +bytes) and the type (hex or decimal). + +If there is a comment, the last byte of the operand field will be an +ASCII semicolon. The remaining bytes on the line are the comment in +ASCII form. + +Tokenized strings can occur in the label or operand parts of the line. +The first byte is the length of the string in bytes, with the high bit +set (e.g. $84 for a 4-byte string), followed by that number of ASCII +bytes. The length doesn't include the first byte, so e.g. the string +"ABC" is stored as $83, $41, $42, $43. Mac/65 doesn't allow empty +strings, so a zero-length string (length byte $80) is an error. + +There are separate sets of tokens for mnemonics/pseudo-ops and operands. +Mnemonic/pseudo-op tokens run from 0 to $5F, and operand tokens run from +0 to $4D (with 0-$09 being "special", and a few invalid tokens in the +range $0A-$4D). See the C source for the full list (or a hex/ascii dump +of the Mac/65 ROM, which is where I got the lists to put them in the C +source). Also, you can run unmac65 with the -v option to get a +line-by-line hex dump of the tokens. + +DIAGNOSTICS +=========== + +unmac65: line XX contains NN non-printable ATASCII characters <= $1F + +Self-explanatory. Depending on what you're going to use the converted +file for, this may or may not be a problem. Non-fatal. This warning +doesn't occur in the Atari version of unmac65. Also, it doesn't occur if +the -u option is in use. + +unmac65: line XX contains NN inverse ATASCII characters >= $80 + +Self-explanatory. Depending on what you're going to use the converted +file for, this may or may not be a problem. Non-fatal. Use the -i option +to convert inverse video to normal. This warning doesn't occur in the +Atari version of unmac65. Also, it doesn't occur if any of the -i, -m, +or -r options are in use. + +unmac65: not a mac/65 file (missing $FEFE header) + +Self-explanatory. Fatal error. + +unmac65: corrupt or truncated file? + +The length of the last line in the file is longer that the number of +bytes remaining in the file (according to the file header). Fatal error. + +unmac65: unexpected EOF? + +The file is shorter than the file header's program length. Probably the +file is truncated; less probably, the length header got scrambled +somehow. Fatal error. + +unmac65: file is too short (N bytes) + +The minimum length for a Mac/65 file is 4 bytes (which would be an empty +program containing no lines). The input file was shorter than 4 bytes. +Fatal error. (Actually, Mac/65 will never produce a 4-byte file, it's +just the theoretical minimum) + +unmac65: file is valid but contains no lines of code + +Self-explanatory. Mac/65 creates a file like this if the SAVE command is +given before entering any code (at startup or after a NEW). The SAVEd +file will be 5 bytes in length, and utterly useless. Non-fatal warning. + +unmac65: line #lll <= prev line #mmm + +The line numbers in the file are supposed to be stored in ascending +order. Somehow this file has the line numbers out of order. Non-fatal, +but probably the rest of the file will be garbage. + +unmac65: internal error, state n + +This is a "this should never happen" error. It indicates a bug in the +program. If you ever see this error, please notify the author, and send +a copy of the Mac/65 program that caused it. This is a non-fatal error, +but the output might be garbage. + +[$nn?] or <$nn?> in the output (where nn is 2 hex digits) + +These indicate unknown/invalid tokens. Either the file is damaged, or +there is a bug in the program. These are non-fatal errors. If you ever +see them, please contact the author, and send a copy of the Mac/65 +program that caused them. + +unmac65: ignoring extra junk at EOF + +The file contains more bytes than the program length header says it +should. This usually means the file was stored on an old DOS disk or +transferred with a broken XMODEM implementation, and was padded to the +sector/block size. Alternately, the header bytes got corrupted somehow +(this is highly unlikely, especially if there are no other +errors/warnings). Non-fatal error. + +Other errors are possible (e.g. disk full, I/O error reading input), but +they're not specific to unmac65; no need to list them all there. + +Fatal errors result in unmac65 terminating. A non-fatal error can +usually be recovered from, though the line that caused it will probably +be printed strangely. + +It's probably worth mentioning also that Mac/65 source files can contain +ATASCII graphics or escape codes, although it's not a very common +practice. If you see strange stuff and/or your terminal misbehaves when +writing to standard output, try writing to a file (-o option) instead. +See also the -m, -r, -u options for human-readable output. The Atari +version will render ATASCII graphics just fine, of course. + +EXIT STATUS +=========== + +unmac65 will normally exit with a zero (success) status upon completion. +A non-zero status indicates a fatal error. + +LIMITATIONS +=========== + +The main difference between Mac/65's LIST output and unmac65's output is +that Mac/65 lines up the label, mnemonic, and comment fields (if the +field contents are short enough to fit in the allotted width), while +unmac65 makes no attempt to do so. If the field alignment is important +to you, try the -t or -ta options (which insert hard tabs, unlike +mac65's spaces). A future version of unmac65 may correct this minor +flaw, but as it stands, I've tested quite a few .M65 files by running +them through unmac65, then ENTERing unmac65's listed file in Mac/65 and +reSAVEing them... In all cases, the newly created tokenized files +compare identically to the original .M65 file. If you come across a file +that doesn't do this, yet is valid (can be assembled without error by +Mac/65), please send it to me, so I can fix unmac65! + +Although a few helpful options have been added for porting Mac/65 +sources to other assemblers, unmac65 doesn't completely automate the +process. Depending on the assembler you're using, you may still have a +lot of manual edits to make. A future version of unmac65 may add a few +more options, but some hypothetical complex porting functions (like +"convert to ca65 format") would require implementing a much more complex +parser (such as a yacc-based recursive descent parser). Most likely this +will never happen, if only because I want the program to be usable on +the Atari itself, with its limited memory and C compiler support. + +There are a few ways that an invalid file can sneak past unmac65's error +checking. The same files wouldn't load correctly in Mac/65 either, but +generally don't cause any errors in Mac/65 (just silent failure). It'd +be nice if unmac65 could act as a "M65 lint" for Mac/65 users. + +unmac65 is mostly developed/tested against the OSS Mac/65 v1.01 +cartridge. The various disk versions appear to use the same tokenized +format, but haven't been well tested. If you have problems, please +contact the author. + +(A further limitation is that the documentation isn't very concise. +Sorry about that.) + +.. include:: manftr.rst @@ -0,0 +1 @@ +.. |version| replace:: 0.2.1 @@ -0,0 +1,313 @@ +#include <stdio.h> +#include <errno.h> +#include <string.h> + +#include "xex.h" + +int xex_errno; +static int xex_sys_errno; + +int xex_verbose = 0; + +static char *errors[] = { + "OK", /* XERR_NONE 0 */ + "Failed system call", /* XERR_SYSCALL 1 */ + "End address < start address", /* XERR_REVERSED 2 */ + "Truncated segment", /* XERR_TRUNCATED 3 */ + "No data", /* XERR_NULLDATA 4 */ + "Unknown/Invalid XEX error code", /* XERR_MAXERR 5 */ +}; + + +int xex_new_seg(xex_segment *seg, unsigned char *data) { + int offset = 0; + + if(data[0] == 0xff && data[1] == 0xff) { + offset = 2; + seg->has_ff_header = 1; + } else { + seg->has_ff_header = 0; + } + + seg->start_addr = data[offset] + (data[offset + 1] << 8); + offset += 2; + + seg->end_addr = data[offset] + (data[offset + 1] << 8); + offset += 2; + + seg->object = &data[offset]; + + seg->len = seg->end_addr - seg->start_addr + 1; + + return xex_check_seg(seg); +} + +int xex_init_seg(xex_segment *seg, unsigned char *object, unsigned short addr) { + object[0] = XEX_LSB(addr); + object[1] = XEX_MSB(addr); + + seg->start_addr = XEX_INITAD; + seg->end_addr = XEX_INITAD + 1; + seg->object = object; + seg->len = 2; + seg->has_ff_header = 0; + + if(xex_verbose) { + fprintf(stderr, "xex_init_seg(): created init segment @ %04X:\n", addr); + xex_print_seg_info(seg); + } + + return 1; +} + +int xex_run_seg(xex_segment *seg, unsigned char *object, unsigned short addr) { + object[0] = XEX_LSB(addr); + object[1] = XEX_MSB(addr); + + seg->start_addr = XEX_RUNAD; + seg->end_addr = XEX_RUNAD + 1; + seg->object = object; + seg->len = 2; + seg->has_ff_header = 0; + + if(xex_verbose) { + fprintf(stderr, "xex_run(): created run segment @ %04X:\n", addr); + xex_print_seg_info(seg); + } + + return 1; +} + +static int read_char(FILE *file) { + int c; + + c = getc(file); + if(c == EOF) { + if(ferror(file)) { + xex_sys_errno = errno; + xex_errno = XERR_SYSCALL; + } else { + xex_errno = XERR_TRUNCATED; + } + } + + return c; +} + +int xex_fread_seg_header(xex_segment *seg, FILE *file) { + int c, d; + unsigned short addr; + + xex_errno = XERR_NONE; + + seg->has_ff_header = 0; + seg->object = NULL; + + c = read_char(file); + if(c == EOF) { + if(feof(file)) + xex_errno = XERR_NONE; + + return 0; + } + d = read_char(file); + if(d == EOF) return 0; + + addr = XEX_ADDR(c, d); + if(addr == 0xffff) { + seg->has_ff_header = 1; + c = read_char(file); + if(c == EOF) return 0; + d = read_char(file); + if(d == EOF) return 0; + addr = XEX_ADDR(c, d); + } + + if(addr == 0xffff) { + xex_errno = XERR_REVERSED; + return 0; + } + + seg->start_addr = addr; + + c = read_char(file); + if(c == EOF) return 0; + d = read_char(file); + if(d == EOF) return 0; + addr = XEX_ADDR(c, d); + seg->end_addr = addr; + + seg->len = seg->end_addr - seg->start_addr + 1; + + if(seg->end_addr < seg->start_addr) { + xex_errno = XERR_REVERSED; + return 0; + } + + if(xex_verbose) { + fprintf(stderr, "xex_fread_seg_header(): read header:\n"); + xex_print_seg_info(seg); + } + + return 1; +} + +int xex_fread_seg_data(xex_segment *seg, FILE *file) { + int res; + + xex_errno = XERR_NONE; + + res = fread(seg->object, 1, seg->len, file); + xex_sys_errno = errno; + + if(xex_verbose) { + fprintf(stderr, "xex_fread_seg_data(): read data:\n"); + xex_print_seg_info(seg); + } + + if(res == seg->len) + return 1; + + if(ferror(file)) { + xex_errno = XERR_SYSCALL; + } else { + /* EOF or short read */ + xex_errno = XERR_TRUNCATED; + } + + return xex_check_seg(seg); +} + +int xex_fread_seg(xex_segment *seg, FILE *file) { + unsigned char *tmp = seg->object; + int res; + + xex_errno = XERR_NONE; + + if(tmp == NULL) { + if(xex_verbose) + fprintf(stderr, "xex_fread_seg(): seg->object == NULL\n"); + xex_errno = XERR_NULLDATA; + return 0; + } + + res = xex_fread_seg_header(seg, file); + seg->object = tmp; + + if(!res) + return 0; + + if(!xex_fread_seg_data(seg, file)) + return 0; + + return 1; +} + +int xex_fwrite_seg(xex_segment *seg, FILE *file) { + int res; + + xex_errno = XERR_NONE; + + if(xex_verbose) { + fprintf(stderr, "xex_fwrite_seg(): about to write:\n"); + xex_print_seg_info(seg); + } + + if(!xex_check_seg(seg)) + return 0; + + if(seg->has_ff_header) { + putc(0xff, file); + putc(0xff, file); + } + + putc(XEX_LSB(seg->start_addr), file); + putc(XEX_MSB(seg->start_addr), file); + putc(XEX_LSB(seg->end_addr), file); + putc(XEX_MSB(seg->end_addr), file); + + res = fwrite(seg->object, 1, seg->len, file); + if(res == seg->len) + return 1; + + if(ferror(file)) { + xex_errno = XERR_SYSCALL; + xex_sys_errno = errno; + } else { + xex_errno = XERR_TRUNCATED; + } + + return 0; +} + +int xex_get_object(xex_segment *seg, unsigned char *data) { + int offset = 0; + + if(!xex_check_seg(seg)) + return 0; + + if(seg->has_ff_header) { + data[0] = data[1] = 0xff; + offset = 2; + } + + data[offset] = XEX_LSB(seg->start_addr); + data[offset + 1] = XEX_MSB(seg->start_addr); + offset += 2; + data[offset] = XEX_LSB(seg->end_addr); + data[offset + 1] = XEX_MSB(seg->end_addr); + offset += 2; + + memcpy(&data[offset], seg->object, seg->len); + + return 1; +} + +int xex_check_seg(xex_segment *seg) { + int ret = 1; + + xex_errno = XERR_NONE; + + if(seg->end_addr < seg->start_addr) { + xex_errno = XERR_REVERSED; + ret = 0; + } + + if(seg->len != (seg->end_addr - seg->start_addr) + 1) { + xex_errno = XERR_TRUNCATED; + ret = 0; + } + + if(seg->object == NULL) { + xex_errno = XERR_NULLDATA; + ret = 0; + } + + if(xex_verbose && !ret && xex_errno != XERR_NULLDATA) + fprintf(stderr, "xex_check_seg() FAILED: %s\n", xex_strerror(xex_errno)); + + return ret; +} + +char *xex_strerror(int err) { + if(err < 0 || err >= XERR_MAXERR) + err = XERR_MAXERR; + + if(err == XERR_SYSCALL) + return strerror(xex_sys_errno); + else + return errors[err]; +} + +void xex_perror(char *msg) { + fprintf(stderr, "%s: %s\n", msg, xex_strerror(xex_errno)); +} + +void xex_print_seg_info(xex_segment *seg) { + xex_check_seg(seg); + fprintf(stderr, + "has_ff_header==%d start==%04X end==%04X " + "len==%04X status==%s\n", + seg->has_ff_header, seg->start_addr, seg->end_addr, + seg->len, xex_strerror(xex_errno)); +} @@ -0,0 +1,117 @@ +#ifndef XEX_H +#define XEX_H + +/* A xex_segment represents one segment of a XEX file. */ +typedef struct { + unsigned short start_addr; + unsigned short end_addr; + unsigned char *object; + int len; + unsigned char has_ff_header; +} xex_segment; + +/* object points to an array containing ONLY the data bytes, not the + header bytes! Use xex_get_object() to reconstitute the full + segment with headers. + + (Note that I'm using the word "object" in the sense of "object code", + not in its OOP sense) + */ + +#define XEX_RUNAD 0x2e0 +#define XEX_INITAD 0x2e2 + +#define XEX_LSB(x) (x & 0xff) +#define XEX_MSB(x) ((x >> 8) & 0xff) +#define XEX_ADDR(x, y) ((unsigned char)x | ((unsigned char)y << 8)) + +/* All int functions return true for success or false for failure. + On failure, the variable xex_errno will be set to one of the + XERR_* constants. Caller may use xex_strerror(xex_errno) to + get a human-readable error message (or xex_perror()). + + Note that XERR_NONE == 0, so you can use the construct: + if(xex_errno) { + // handle error here + } + */ + +#define XERR_NONE 0 +#define XERR_SYSCALL 1 +#define XERR_REVERSED 2 +#define XERR_TRUNCATED 3 +#define XERR_NULLDATA 4 +#define XERR_MAXERR 5 + +extern int xex_errno; +extern int xex_verbose; + +/* Initialize a xex_segment from the data pointed to by segment, which must + not be null. segment must be a complete segment, including start/end + addresses and possible $FFFF header. seg->object will point within the + data, so don't free() it until you're done with seg! */ +int xex_new_seg(xex_segment *seg, unsigned char *segment); + +/* Set up seg as an init address segment. object must have room for at + least 2 characters. A copy of the object pointer is stored in + seg->object, so don't free(object) until you're done with seg. */ +int xex_init_seg(xex_segment *seg, unsigned char *object, unsigned short addr); + +/* Set up seg as an run address segment. object must have room for at + least 2 characters. A copy of the object pointer is stored in seg->object, + so don't free(object) until you're done with seg.*/ +int xex_run_seg(xex_segment *seg, unsigned char *object, unsigned short addr); + +/* Since this library does NOT allocate any memory, reading a segment must + be done in two pieces. xex_fread_seg_header() reads the 4- or 6-byte + header and sets seg->len to the size of the segment. xex_fread_seg_data() + then reads this number of bytes from the file, into seg->object. + + Example usage: + + xex_segment seg; + + xex_fread_seg_header(&seg, my_file); + seg.object = malloc(seg->len); + xex_fread_seg_data(&seg, my_file); + + // ...later, the caller must free(seg.object) when done with it. + + xex_fread_seg_header() returns 0 on EOF, with xex_errno set to + XERR_NONE. It also returns 0 on failure, but xex_errno will be + something other than XERR_NONE. +*/ +int xex_fread_seg_header(xex_segment *seg, FILE *file); +int xex_fread_seg_data(xex_segment *seg, FILE *file); + +/* If you're dealing with xex files sequentially, and want to use a + static buffer instead of malloc(), you can use xex_fread_seg() instead. + seg->object should point to a buffer large enough to hold any segment's + object code (64K, to be on the safe side), or you WILL get segfaults. */ +int xex_fread_seg(xex_segment *seg, FILE *file); + +/* xex_fwrite_seg() writes a segment to a file, which must be opened + for output. */ +int xex_fwrite_seg(xex_segment *seg, FILE *file); + +/* Extract the object code from a segment. data must have room for len+6 + bytes (or len+4 if has_ff_header is false). data will be the raw + segment data, suitable for writing to a XEX file. */ +int xex_get_object(xex_segment *seg, unsigned char *data); + +/* Sanity check a segment. Make sure start_addr is less than end_addr, + that it doesn't wrap around the 6502 address space, and so on. + Returns true if segment is OK. */ +int xex_check_seg(xex_segment *seg); + +/* Get human-readable error message. If xex_errno is XSYSCALL, the system's + strerror() is called. */ +char *xex_strerror(int err); + +/* Shortcut for xex_strerror(), like system's perror() */ +void xex_perror(char *msg); + +/* Debugging aid */ +void xex_print_seg_info(xex_segment *seg); + +#endif /* XEX_H */ diff --git a/xexcat.1 b/xexcat.1 new file mode 100644 index 0000000..5cb659c --- /dev/null +++ b/xexcat.1 @@ -0,0 +1,181 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "XEXCAT" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +xexcat \- Concatenate Atari 8-bit executables (XEX) into a single XEX file. +.\" RST source for xexcat(1) man page. Convert with: +. +.\" rst2man.py xexcat.rst > xexcat.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fIxexcat\fP [\fI\-hvc\fP] [\-l \fIaddress\fP [\-r \fIaddress\fP] [\-i \fIaddress\fP] [\-o \fIoutfile.xex\fP] [\fIinfile.xex\fP] [\fIinfile.xex ...\fP] +.SH DESCRIPTION +.sp +\fBxexcat\fP reads one or more Atari executables (XEX/BIN/COM/etc) +from the given filenames, and writes a single Atari executable +containing all the segments from all the input files to \fIoutfile\fP\&. +.sp +To read from standard input, \fIinfile\fP may be omitted, or given as +\fB\-\fP\&. To write to standard output, \fB\-o\fP \fIoutfile\fP may be omitted, +or given as \fB\-o\-\fP\&. +.sp +The output file is a valid Atari executable, including the +required \fI$FFFF\fP header for the first segment. If there are multiple +segments, the second and subsequent segments will not have the +optional \fI$FFFF\fP header. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-h +Print a short help message and exit. +.TP +.B \-v +Verbose operation. Each segment\(aqs information is printed to +standard error, including start/end address and length. +.TP +.B \-c +Check only; no output file is written. Equivalent to \fB\-v \-o /dev/null\fP\&. +.TP +.BI \-o \ outfile +Write output xex file to outfile. Default is to write to standard output. +.TP +.BI \-l \ address +Force the output file\(aqs load address to address. This only +affects the first segment of the output file. +.TP +.BI \-i \ address +Force the output file\(aqs first init address (if present) to +\fIaddress\fP\&. This \fIonly\fP affects the \fBfirst\fP init address segment of the +output file. Further init address segments in the input will be +left unmodified. If \fIaddress\fP is 0, the first init segment will +be removed (0 means "none", not "init at address 0"). This option +does nothing if none of the input files contain init address +segments. +.TP +.BI \-r \ address +Force the output file\(aqs run address to \fIaddress\fP\&. If \fIaddress\fP +is not 0, all run address segments from all input files will be +ignored, and a new run address segment will be constructed +with the given \fIaddress\fP and appended to the output. If \fIaddress\fP +is 0, all run addresses from all input files are ignored, +and the output file will not contain a run address segment +at all. Such a file can still be loaded from DOS, but it will +not execute (user will be returned to the DOS menu). +.UNINDENT +.SH NOTES +.sp +It is possible to join multiple Atari executables together with +the standard \fBcat\fP(1) command. However, \fBxexcat\fP is always guaranteed to +produce a valid Atari binary load file (or an empty file, if all input +files are invalid), which is not the case for \fBcat\fP\&. +.sp +When writing to standard output, \fBxexcat\fP will refuse to write +binary data to the user\(aqs terminal. +.sp +The Atari binary load format requires the \fI$FFFF\fP header only for +the first segment in a file. The second and subsequent segments +may also have a \fI$FFFF\fP header, but it\(aqs optional. \fBxexcat\fP\(aqs output file +will always have the \fI$FFFF\fP header for the first segment, and no +\fI$FFFF\fP header for further segments, regardless of whether the segments +in the input files had it or not (in fact, \fBxexcat\fP can handle +an invalid XEX file which is missing the initial $FFFF header for the +first segment). +.sp +Some Atari executables contain raw blocks of data, which are meant +to be read into memory by the init routine. These blocks do not +have start/end address headers, so \fBxexcat\fP is unable to handle +them. Raw data blocks usually occur in files created with "packer" +or "compressor" programs, or occasionally in other large programs +(Turbo BASIC is an example). Raw data blocks are generally found just +after an init address segment. If you have an executable that loads +just fine on a real Atari or emulator, but fails with \fBxexcat\fP, +a raw data block is usually the reason why. +.sp +The terms "Atari executable", "binary load file", and "XEX file" +all refer to the same thing. Also, there is no difference between +Atari executables named with "XEX", "COM", "BIN", "EXE", etc. The +Atari and its DOS don\(aqt care about the names, only the contents. +.SH EXIT STATUS +.sp +Exit status is zero for success, non\-zero for failure. +.\" other sections we might want, uncomment as needed. +. +.\" FILES +. +.\" ===== +. +.\" ENVIRONMENT +. +.\" =========== +. +.\" EXIT STATUS +. +.\" =========== +. +.\" BUGS +. +.\" ==== +. +.\" EXAMPLES +. +.\" ======== +. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/xexcat.c b/xexcat.c new file mode 100644 index 0000000..22d8276 --- /dev/null +++ b/xexcat.c @@ -0,0 +1,248 @@ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> + +#include "get_address.h" +#include "xex.h" + +#ifndef VERSION +#define VERSION "???" +#endif + +#define SELF "xexcat" +#define OPTIONS "hvo:l:r:i:c" + +char *usage = + SELF " v" VERSION " - by B. Watson (WTFPL)\n" + "Join one or more Atari 8-bit executables into one multi-segment file.\n" + "usage: " SELF " -[hvc] [-l address] [-i address] [-r address]\n" + " [-o outfile.xex] [infile1.xex] [infile2.xex ...]\n" + " -h Print this help\n" + " -o outfile.xex Output file (default: standard output)\n" + " -v Verbose operation\n" + " -c Check only; no output (same as -v -o/dev/null)\n" + " -l address Force first load address (decimal, $hex, or 0xhex)\n" + " -i address Force first init address\n" + " -r address Force run address\n"; + +int main(int argc, char **argv) { + xex_segment seg; + char *outfile = "-"; + FILE *in, *out = stdout; + int count = 1, c, errcount = 0; + unsigned char buffer[65536]; /* be lazy: statically allocate large buffer */ + int force_load = -1, force_run = -1, force_init = -1; + int read_stdin = 0; + + /* parse args */ + while( (c = getopt(argc, argv, OPTIONS)) > 0) { + switch(c) { + case 'h': + printf(usage); + exit(0); + break; + + case 'v': + xex_verbose = 1; + break; + + case 'o': + outfile = optarg; + break; + + case 'c': + xex_verbose = 1; + outfile = "/dev/null"; + break; + + case 'l': + if( (force_load = get_address(SELF, optarg)) < 0 ) + exit(1); + break; + + case 'r': + if( (force_run = get_address(SELF, optarg)) < 0 ) + exit(1); + break; + + case 'i': + if( (force_init = get_address(SELF, optarg)) < 0 ) + exit(1); + break; + + default: + fprintf(stderr, usage); + exit(1); + } + } + + /* special case if there are no input filenames */ + if(argc <= optind) { + read_stdin = 1; + } + + /* open outfile */ + if(strcmp(outfile, "-") != 0) { + if( !(out = fopen(outfile, "wb")) ) { + fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno)); + exit(1); + } + } else if(isatty(fileno(out))) { + /* be polite... */ + fprintf(stderr, + SELF ": Standard output is a terminal; not writing binary data\n" + "Run '" SELF " -h' for help\n"); + exit(1); + } else { + outfile = "(standard output)"; + } + + /* only have to set seg.object once... */ + seg.object = buffer; + + /* process each input file on the command line */ + while(read_stdin || (optind < argc)) { + char *infile = argv[optind++]; + int filecount = 1; + + if(read_stdin || strcmp(infile, "-") == 0) { + read_stdin = 0; + in = stdin; + infile = "(standard input)"; + } else if( !(in = fopen(infile, "rb")) ) { + /* failure to open is NOT fatal (just skip it and move on) */ + fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno)); + errcount++; + continue; + } + + if(xex_verbose) + fprintf(stderr, SELF ": reading from file %s\n", infile); + + /* process every segment in current input file */ + while(xex_fread_seg(&seg, in)) { + if(filecount++ == 1 && !seg.has_ff_header) { + fprintf(stderr, SELF ": warning: '%s' first segment " + "lacks $FFFF header (bad/partial XEX file?)\n", + infile); + } + + /* normalize the $FFFF headers: only the first segment needs one */ + seg.has_ff_header = (count == 1); + + /* process -l option */ + if(count == 1 && force_load > -1) { + if(xex_verbose) + fprintf(stderr, + SELF ": %s: setting first load address to %04X " + "due to -l option\n", + infile, force_load); + seg.start_addr = force_load; + seg.end_addr = force_load + seg.len - 1; + force_load = -1; + } + + count++; + + /* process -i option */ + if(seg.start_addr == XEX_INITAD && seg.len == 2) { + if(force_init == 0) { + if(xex_verbose) + fprintf(stderr, + SELF ": %s: " + "skipping first init address segment due to -i0\n", + infile); + force_init = -1; + continue; + } else if(force_init > 0) { + if(xex_verbose) + fprintf(stderr, + SELF ": %s: " + "setting first init address to %04X due to -i option\n", + infile, force_init); + seg.object[0] = XEX_LSB(force_init); + seg.object[1] = XEX_MSB(force_init); + force_init = -1; + } + + if(xex_verbose) + fprintf(stderr, + SELF ": %s: init address: %04X\n", + infile, XEX_ADDR(seg.object[0], seg.object[1])); + } + + /* process -r option */ + if(seg.start_addr == XEX_RUNAD && seg.len == 2) { + if(force_run > -1) { + if(xex_verbose) + fprintf(stderr, + SELF ": %s: " + "skipping run address segment due to -r option\n", + infile); + continue; + } else { + if(xex_verbose) + fprintf(stderr, + SELF ": %s: " + "run address: %04X\n", + infile, XEX_ADDR(seg.object[0], seg.object[1])); + } + } + + /* write (possibly modified) segment to output */ + if(!xex_fwrite_seg(&seg, out)) { + fprintf(stderr, SELF ": %s: %s\n", + outfile, xex_strerror(xex_errno)); + xex_errno = 0; + errcount++; + break; + } + } + + /* xex_errno will be 0 for XERR_NONE (meaning "no error") */ + if(xex_errno) { + fprintf(stderr, SELF ": %s: %s\n", + infile, xex_strerror(xex_errno)); + errcount++; + } else if(filecount == 1) { + fprintf(stderr, SELF ": warning: %s: file is empty.\n", infile); + } + + if(xex_verbose) + fprintf(stderr, SELF ": done reading file %s\n", infile); + + fclose(in); + } + + /* if -r given, all run addresses in all files were omitted from the + output file. Here we add a single run address to the output. */ + if(force_run > 0) { + xex_run_seg(&seg, buffer, force_run); + if(!xex_fwrite_seg(&seg, out)) { + fprintf(stderr, SELF ": %s: %s\n", + outfile, xex_strerror(xex_errno)); + errcount++; + } + count++; + } else if(count == 1) { + fprintf(stderr, SELF ": warning: %s: file is empty.\n", outfile); + } + + if(xex_verbose) + fprintf(stderr, SELF ": wrote %d segment%s to %s\n", + (count - 1), + (count == 2 ? "" : "s"), + outfile); + + fclose(out); + + if(xex_verbose || errcount) { + fprintf(stderr, + SELF ": %d error%s.\n", errcount, (errcount == 1 ? "" : "s")); + return errcount; + } + + return 0; +} diff --git a/xexcat.rst b/xexcat.rst new file mode 100644 index 0000000..a7ce20b --- /dev/null +++ b/xexcat.rst @@ -0,0 +1,132 @@ +.. RST source for xexcat(1) man page. Convert with: +.. rst2man.py xexcat.rst > xexcat.1 +.. rst2man.py comes from the SBo development/docutils package. + +====== +xexcat +====== + +----------------------------------------------------------------- +Concatenate Atari 8-bit executables (XEX) into a single XEX file. +----------------------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +*xexcat* [*-hvc*] [-l *address* [-r *address*] [-i *address*] [-o *outfile.xex*] [*infile.xex*] [*infile.xex ...*] + +DESCRIPTION +=========== + +**xexcat** reads one or more Atari executables (XEX/BIN/COM/etc) +from the given filenames, and writes a single Atari executable +containing all the segments from all the input files to *outfile*. + +To read from standard input, *infile* may be omitted, or given as +**-**. To write to standard output, **-o** *outfile* may be omitted, +or given as **-o-**. + +The output file is a valid Atari executable, including the +required *$FFFF* header for the first segment. If there are multiple +segments, the second and subsequent segments will not have the +optional *$FFFF* header. + +OPTIONS +======= + +-h + Print a short help message and exit. + +-v + Verbose operation. Each segment's information is printed to + standard error, including start/end address and length. + +-c + Check only; no output file is written. Equivalent to **-v -o /dev/null**. + +-o outfile + Write output xex file to outfile. Default is to write to standard output. + +-l address + Force the output file's load address to address. This only + affects the first segment of the output file. + +-i address + Force the output file's first init address (if present) to + *address*. This *only* affects the **first** init address segment of the + output file. Further init address segments in the input will be + left unmodified. If *address* is 0, the first init segment will + be removed (0 means "none", not "init at address 0"). This option + does nothing if none of the input files contain init address + segments. + +-r address + Force the output file's run address to *address*. If *address* + is not 0, all run address segments from all input files will be + ignored, and a new run address segment will be constructed + with the given *address* and appended to the output. If *address* + is 0, all run addresses from all input files are ignored, + and the output file will not contain a run address segment + at all. Such a file can still be loaded from DOS, but it will + not execute (user will be returned to the DOS menu). + +NOTES +===== + +It is possible to join multiple Atari executables together with +the standard **cat**\(1) command. However, **xexcat** is always guaranteed to +produce a valid Atari binary load file (or an empty file, if all input +files are invalid), which is not the case for **cat**. + +When writing to standard output, **xexcat** will refuse to write +binary data to the user's terminal. + +The Atari binary load format requires the *$FFFF* header only for +the first segment in a file. The second and subsequent segments +may also have a *$FFFF* header, but it's optional. **xexcat**'s output file +will always have the *$FFFF* header for the first segment, and no +*$FFFF* header for further segments, regardless of whether the segments +in the input files had it or not (in fact, **xexcat** can handle +an invalid XEX file which is missing the initial $FFFF header for the +first segment). + +Some Atari executables contain raw blocks of data, which are meant +to be read into memory by the init routine. These blocks do not +have start/end address headers, so **xexcat** is unable to handle +them. Raw data blocks usually occur in files created with "packer" +or "compressor" programs, or occasionally in other large programs +(Turbo BASIC is an example). Raw data blocks are generally found just +after an init address segment. If you have an executable that loads +just fine on a real Atari or emulator, but fails with **xexcat**, +a raw data block is usually the reason why. + +The terms "Atari executable", "binary load file", and "XEX file" +all refer to the same thing. Also, there is no difference between +Atari executables named with "XEX", "COM", "BIN", "EXE", etc. The +Atari and its DOS don't care about the names, only the contents. + +EXIT STATUS +=========== + +Exit status is zero for success, non-zero for failure. + +.. other sections we might want, uncomment as needed. + +.. FILES +.. ===== + +.. ENVIRONMENT +.. =========== + +.. EXIT STATUS +.. =========== + +.. BUGS +.. ==== + +.. EXAMPLES +.. ======== + +.. include:: manftr.rst diff --git a/xexsplit.1 b/xexsplit.1 new file mode 100644 index 0000000..c3a88e6 --- /dev/null +++ b/xexsplit.1 @@ -0,0 +1,195 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "XEXSPLIT" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +xexsplit \- Split a multi-segment Atari 8-bit executable (XEX) into multiple single-segment files. +.\" RST source for xexsplit(1) man page. Convert with: +. +.\" rst2man.py xexsplit.rst > xexsplit.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.\" TODO: add -n option (same as -v, but no output)? +. +.SH SYNOPSIS +.sp +\fIxexsplit\fP [\fI\-hv\fP] \fIinfile.xex\fP [\fIoutfile\-prefix\fP] +.SH DESCRIPTION +.sp +xexsplit reads an Atari executable (XEX/BIN/COM/etc) from \fIinfile\fP and +writes each segment to a separate file. To read from standard input, +infile may be omitted, or given as \fB\-\fP\&. +.sp +Output files are named \fBoutfile\-prefix.NNN.SSSS.EEEE\fP, where \fBNNN\fP is +the segment number (decimal, 3 digits, with leading zeroes), \fBSSSS\fP +is the start address of the segment (4 digits, hex), and \fBEEEE\fP is the +end address of the segment (4 digits, hex). If \fIoutfile\-prefix\fP is +omitted, the default prefix is \fBxexsplit.xex\fP\&. +.sp +Each output file is a valid single\-segment Atari executable, including +the required \fI$FFFF\fP header. They may be joined back together (possibly +after being modified) with xexcat or plain old \fBcat\fP(1). +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-h +Print a short help message and exit. +.TP +.B \-v +Verbose operation. Each segment\(aqs information is printed to +standard error, including start/end address and length. +.UNINDENT +.SH NOTES +.sp +The terms "Atari executable", "binary load file", and "XEX file" +all refer to the same thing. Also, there is no difference between +Atari executables named with "XEX", "COM", "BIN", "EXE", etc. The +Atari and its DOS don\(aqt care about the names, only the contents. +.sp +It is not possible to use \fB\-\fP to write to standard output. This makes +sense, because there\(aqs no way to write multiple, separate files to stdout. +.sp +The output filenames were chosen so they will sort in the order they +were created, when used with the shell\(aqs globbing. This means that you +can join them back into a single valid XEX file with a command like: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +cat prefix.* > joined.xex +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +or use \fBxexcat\fP(1) instead of \fBcat\fP: +.INDENT 0.0 +.INDENT 3.5 +.sp +.nf +.ft C +xexcat \-o joined.xex prefix.* +.ft P +.fi +.UNINDENT +.UNINDENT +.sp +The Atari binary load format requires the \fI$FFFF\fP header only for +the first segment in a file. The second and subsequent segments may +also have a \fI$FFFF\fP header, but it\(aqs optional. \fBxexsplit\fP\(aqs output +files will always have the \fI$FFFF\fP header, regardless of whether +the segments in the original file had it or not (in fact, \fBxexsplit\fP +can handle an invalid XEX file which is missing the initial $FFFF +header for the first seg‐ ment). +.sp +A segment starting at address \fB$02E2\fP contains an init address. +During loading, Atari DOSes JSR to the init address immediately +after its segment has loaded. Init addresses are optinal; there\(aqs no +rule that says that every file has to have one. It\(aqs also normal +and fairly common for there to be several init addresses in a +multi\-segment file. An init ad‐ dress segment is normally located +right after the code segment it\(aqs in‐ tended to run, but this is not +a requirement. +.sp +A segment starting at address \fB$02E0\fP contains a run address. +After all segments have been loaded, Atari DOSes JSR to the run +address to run the program just loaded. The run address is optional; +a file containing no run address segment will simply be loaded into +memory and not executed (user will be returned to the DOS menu). +It\(aqs legal for a multi\-segment file to contain multiple run +address segments, but only the last run address will actually be +used. Normally, there is only one run address; if there are more than +one, only the last one is used. The run address is normally the last +segment in the file, but this is not a requirement. +.sp +Attempting to run a single\-segment file consisting of a run or init +address only, will generally crash the Atari. +.sp +Some Atari executables contain raw blocks of data, which are meant +to be read into memory by the init routine. These blocks do not +have start/end address headers, so \fBxexsplit\fP is unable to handle +them. Raw data blocks usually occur in files created with "packer" +or "compressor" programs, or occasionally in other large programs +(Turbo BASIC is an example). Raw data blocks are generally found just +after an init address segment. If you have an executable that loads +just fine on a real Atari or emulator, but fails with \fBxexsplit\fP, +a raw data block is usually the reason why. +.\" other sections we might want, uncomment as needed. +. +.\" FILES +. +.\" ===== +. +.\" ENVIRONMENT +. +.\" =========== +. +.\" EXIT STATUS +. +.\" =========== +. +.\" BUGS +. +.\" ==== +. +.\" EXAMPLES +. +.\" ======== +. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/xexsplit.c b/xexsplit.c new file mode 100644 index 0000000..4df51f3 --- /dev/null +++ b/xexsplit.c @@ -0,0 +1,123 @@ +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> + +#include "xex.h" + +#ifndef VERSION +#define VERSION "???" +#endif + +#define SELF "xexsplit" +#define OPTIONS "hv" + +char *usage = + SELF " v" VERSION " by B. Watson (WTFPL)\n" + "Split a multi-segment Atari binary load file into multiple files\n" + "usage: " SELF " -[hv] [infile.xex] [outfile-prefix]\n" + " -h Print this help\n" + " -v Verbose operation\n"; + +int main(int argc, char **argv) { + xex_segment seg; + char *infile = "-"; + FILE *in = stdin; + char outfile[4096]; + unsigned char buffer[65536]; + FILE *out = NULL; + int count = 1, outlen, c; + + strcpy(outfile, "xexsplit"); + + while( (c = getopt(argc, argv, OPTIONS)) > 0) { + switch(c) { + case 'h': + printf(usage); + exit(0); + break; + + case 'v': + xex_verbose = 1; + break; + + default: + fprintf(stderr, usage); + exit(1); + } + } + + if(argc > optind) { + infile = argv[optind]; + optind++; + } + + if(argc > optind) { + strcpy(outfile, argv[optind]); + optind++; + + if(argc > optind) + fprintf(stderr, SELF ": " + "ignoring extra junk on command line: '%s'.\n", argv[optind]); + } else if(strcmp(infile, "-") != 0) { + strcpy(outfile, infile); + } + + if(strcmp(outfile, "-") == 0) { + fprintf(stderr, SELF ": can't write to standard output.\n"); + exit(1); + } + + outlen = strlen(outfile); + + if( strcmp(infile, "-") != 0 && !(in = fopen(infile, "rb")) ) { + fprintf(stderr, SELF ": %s: %s\n", infile, strerror(errno)); + exit(1); + } + + seg.object = buffer; + while(xex_fread_seg(&seg, in)) { + if(count == 1 && !seg.has_ff_header) { + fprintf(stderr, SELF ": warning: first segment lacks $FFFF header " + "(bad XEX file?)\n"); + } + + seg.has_ff_header = 1; + + sprintf(outfile + outlen, + ".%03d.%04X.%04X", + count, seg.start_addr, seg.end_addr); + + if( !(out = fopen(outfile, "wb")) ) { + fprintf(stderr, SELF ": %s: %s\n", outfile, strerror(errno)); + fclose(in); + exit(1); + } + + if(!xex_fwrite_seg(&seg, out)) + break; + + fclose(out); + out = NULL; + + fprintf(stderr, SELF ": Wrote file %s\n", outfile); + + count++; + } + + fclose(in); + if(out) fclose(out); + + if(xex_errno) { + xex_perror(SELF); + return 1; + } + + if(count == 1) { + fprintf(stderr, SELF ": input file was empty!\n"); + return 1; + } + + return 0; +} diff --git a/xexsplit.rst b/xexsplit.rst new file mode 100644 index 0000000..3a304c3 --- /dev/null +++ b/xexsplit.rst @@ -0,0 +1,128 @@ +.. RST source for xexsplit(1) man page. Convert with: +.. rst2man.py xexsplit.rst > xexsplit.1 +.. rst2man.py comes from the SBo development/docutils package. + +.. TODO: add -n option (same as -v, but no output)? + +======== +xexsplit +======== + +-------------------------------------------------------------------------------------- +Split a multi-segment Atari 8-bit executable (XEX) into multiple single-segment files. +-------------------------------------------------------------------------------------- + +.. include:: manhdr.rst + +SYNOPSIS +======== + +*xexsplit* [*-hv*] *infile.xex* [*outfile-prefix*] + +DESCRIPTION +=========== + +xexsplit reads an Atari executable (XEX/BIN/COM/etc) from *infile* and +writes each segment to a separate file. To read from standard input, +infile may be omitted, or given as **-**. + +Output files are named **outfile-prefix.NNN.SSSS.EEEE**, where **NNN** is +the segment number (decimal, 3 digits, with leading zeroes), **SSSS** +is the start address of the segment (4 digits, hex), and **EEEE** is the +end address of the segment (4 digits, hex). If *outfile-prefix* is +omitted, the default prefix is **xexsplit.xex**. + +Each output file is a valid single-segment Atari executable, including +the required *$FFFF* header. They may be joined back together (possibly +after being modified) with xexcat or plain old **cat**\(1). + +OPTIONS +======= + +-h + Print a short help message and exit. + +-v + Verbose operation. Each segment's information is printed to + standard error, including start/end address and length. + +NOTES +===== + +The terms "Atari executable", "binary load file", and "XEX file" +all refer to the same thing. Also, there is no difference between +Atari executables named with "XEX", "COM", "BIN", "EXE", etc. The +Atari and its DOS don't care about the names, only the contents. + +It is not possible to use **-** to write to standard output. This makes +sense, because there's no way to write multiple, separate files to stdout. + +The output filenames were chosen so they will sort in the order they +were created, when used with the shell's globbing. This means that you +can join them back into a single valid XEX file with a command like:: + + cat prefix.* > joined.xex + +or use **xexcat**\(1) instead of **cat**:: + + xexcat -o joined.xex prefix.* + +The Atari binary load format requires the *$FFFF* header only for +the first segment in a file. The second and subsequent segments may +also have a *$FFFF* header, but it's optional. **xexsplit**'s output +files will always have the *$FFFF* header, regardless of whether +the segments in the original file had it or not (in fact, **xexsplit** +can handle an invalid XEX file which is missing the initial $FFFF +header for the first seg‐ ment). + +A segment starting at address **$02E2** contains an init address. +During loading, Atari DOSes JSR to the init address immediately +after its segment has loaded. Init addresses are optinal; there's no +rule that says that every file has to have one. It's also normal +and fairly common for there to be several init addresses in a +multi-segment file. An init ad‐ dress segment is normally located +right after the code segment it's in‐ tended to run, but this is not +a requirement. + +A segment starting at address **$02E0** contains a run address. +After all segments have been loaded, Atari DOSes JSR to the run +address to run the program just loaded. The run address is optional; +a file containing no run address segment will simply be loaded into +memory and not executed (user will be returned to the DOS menu). +It's legal for a multi-segment file to contain multiple run +address segments, but only the last run address will actually be +used. Normally, there is only one run address; if there are more than +one, only the last one is used. The run address is normally the last +segment in the file, but this is not a requirement. + +Attempting to run a single-segment file consisting of a run or init +address only, will generally crash the Atari. + +Some Atari executables contain raw blocks of data, which are meant +to be read into memory by the init routine. These blocks do not +have start/end address headers, so **xexsplit** is unable to handle +them. Raw data blocks usually occur in files created with "packer" +or "compressor" programs, or occasionally in other large programs +(Turbo BASIC is an example). Raw data blocks are generally found just +after an init address segment. If you have an executable that loads +just fine on a real Atari or emulator, but fails with **xexsplit**, +a raw data block is usually the reason why. + +.. other sections we might want, uncomment as needed. + +.. FILES +.. ===== + +.. ENVIRONMENT +.. =========== + +.. EXIT STATUS +.. =========== + +.. BUGS +.. ==== + +.. EXAMPLES +.. ======== + +.. include:: manftr.rst diff --git a/xextest.c b/xextest.c new file mode 100644 index 0000000..06614ab --- /dev/null +++ b/xextest.c @@ -0,0 +1,87 @@ +#include <stdio.h> +#include "xex.h" + +/* This is a minimal program illustrating the use of the xex library. + + It expects to read one or more Atari executables from standard + input, and writes one (possibly multi-segment) Atari executable + to standard output. + + This code is provided an an example only; for a useful real-world + tool, use xexcat. + + To compile: "make xextest" if you have the full bw_atari8_utils + source distribution. Otherwise, (assuming you at least have xex.c + and xex.h): gcc -o xextest xextest.c xex.c (should work with other + compilers too). + */ + +int main(int argc, char **argv) { + xex_segment seg; + unsigned char buffer[64 * 1024]; /* 64K buffer is guaranteed big enough */ + int count = 1; + + /* tell xex library to emit debugging trace to stderr. Default value + is 0 (no trace). */ + xex_verbose = 1; + + /* xex_fread_seg_header() returns false on error or at EOF. */ + while(xex_fread_seg_header(&seg, stdin)) { + seg.object = buffer; + + /* We're using a static buffer here. If we were using dynamically + allocated buffers, we'd say "seg.object = malloc(seg.len)" above, + and put a "free(seg.object)" somewhere after the xex_fwrite_seg(). + + Note that the xex lib NEVER calls malloc() or free() itself. + + (Also, it never calls any of the standard I/O functions except + fread(), fwrite(), feof(), or ferror(); you have to fopen() and + fclose() in the calling code). + */ + + /* xex library doesn't care if the first segment is missing the + required Atari $FFFF header, so we handle it ourselves. */ + if(count == 1 && !seg.has_ff_header) + fprintf(stderr, "missing initial $FFFF header (bad XEX file?)\n"); + + /* Force the first segment to have a $FFFF header, and remove the + (optional) $FFFF header from subsequent segments. */ + seg.has_ff_header = (count == 1); + + /* Read the segment data. xex_fread_seg_data() returns false for + failure (with xex_errno set to indicate the reason). An EOF in the + middle of a segment is an error (means the file was truncated). */ + if(!xex_fread_seg_data(&seg, stdin)) + break; + + /* xex_fwrite_seg() returns true for success, or false for failure + (with xex_errno set). Unless you've diddled with seg's fields, + xex_errno will always be XERR_SYSCALL when xex_fwrite_seg() + fails (and xex_perror() or xex_strerror() will call the real + strerror() to get the system's error message). */ + if(!xex_fwrite_seg(&seg, stdout)) + break; + + /* If we weren't using xex_verbose mode, we might want to call + xex_print_seg_info() ourselves to print some info about the + segment, like so: */ + + /* xex_print_seg_info(&seg); */ + + fprintf(stderr, "segment #%d done\n\n", count); + count++; + } + + /* If xex_fread_seg_header() returned false due to EOF, xex_errno will + be zero (aka XERR_NONE). Otherwise, xex_fread_seg_header() didn't like + the header, or else xex_fread_seg_data() returned false (probably + due to a premature EOF in the middle of the segment data), so we print + an error message about it. */ + if(xex_errno) + xex_perror("error"); + + /* xex_errno will be 0 for error or non-zero otherwise, so it's suitable + for use as a standard UNIX exit status. */ + return xex_errno; +} diff --git a/xfd2atr.1 b/xfd2atr.1 new file mode 100644 index 0000000..9e5c541 --- /dev/null +++ b/xfd2atr.1 @@ -0,0 +1,132 @@ +.\" Man page generated from reStructuredText. +. +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.TH "XFD2ATR" 1 "2022-08-27" "0.2.0" "Urchlay's Atari 8-bit Tools" +.SH NAME +xfd2atr \- Convert an Atari 8-bit XFD (raw) disk image to an ATR image. +.\" RST source for xfd2atr(1) man page. Convert with: +. +.\" rst2man.py xfd2atr.rst > xfd2atr.1 +. +.\" rst2man.py comes from the SBo development/docutils package. +. +.SH SYNOPSIS +.sp +\fIxfd2atr\fP [\fI\-sd\fP] \fIinfile.xfd\fP [\fIoutfile.atr\fP] +.SH DESCRIPTION +.sp +\fBxfd2atr\fP generates and adds a 16\-byte ATR header to an XFD +image. If no \fB\-s\fP or \fB\-d\fP options are given, xfd2atr tries to +guess the density based on the file size. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-s +Assume the image uses single density (128\-byte) sectors, instead +of trying to guess the density from the file size. +.TP +.B \-d +Assume the image uses double density (256\-byte) sectors, instead +of trying to guess the density from the file size. +.UNINDENT +.SH NOTES +.sp +You may use \fB\-\fP for \fIinfile\fP to read from standard input and/or +\fB\-\fP for \fIoutfile\fP to write to standard output. If a filename is +supplied for \fIoutfile\fP, it will always be used as\-is (no \fI\&.atr\fP +extension will be appended). +.sp +If \fIoutfile\fP is omitted, it is constructed like so: +.INDENT 0.0 +.INDENT 3.5 +.INDENT 0.0 +.IP \(bu 2 +If reading from standard input, write to standard output. +.IP \(bu 2 +If reading from a file whose name ends with an \fI\&.xfd\fP or \fI\&.XFD\fP +extension, replace the extension with \fI\&.atr\fP\&. +.IP \(bu 2 +Otherwise, append \fI\&.atr\fP to the input filename. +.UNINDENT +.UNINDENT +.UNINDENT +.sp +Since XFD images are raw dumps with no header or structure, it\(aqs +impossible to know the correct density (bytes/sector) for a given +image for certain. +.sp +However, no known Atari\-compatible disk format uses other than 128 +or 256 bytes per sector (or possibly 512, for some hard disk images, +but \fBxfd2atr\fP doesn\(aqt support these). This means file that isn\(aqt a +multiple of 128 bytes in size will be rejected. +.sp +Likewise, no known format uses an odd number of sectors, and +it\(aqs assumed that all double\-density images will begin with 3 +single\-density boot sectors (true of all floppy images you\(aqre +ever likely to run across; may not be true of hard disk images). +.sp +Given these assumptions, \fBxfd2atr\fP is able to make an educated +guess about the correct sector size and count to use for the ATR +header it generates. If it guesses wrong, the resulting ATR image +will be unusable; if this happens, re\-run \fBxfd2atr\fP and force the +density with \fB\-s\fP or \fB\-d\fP\&. +.SH EXIT STATUS +.sp +Exit status is zero for success, non\-zero for failure. Further, +exit status will be 1 for errors involving file I/O (file not found, +permissions, etc), and 2 for structural errors in the XFD file. +.SH COPYRIGHT +.sp +WTFPL. See \fI\%http://www.wtfpl.net/txt/copying/\fP for details. +.SH AUTHOR +.INDENT 0.0 +.IP B. 3 +Watson <\fI\%urchlay@slackware.uk\fP>; Urchlay on irc.libera.chat \fI##atari\fP\&. +.UNINDENT +.SH SEE ALSO +.sp +\fBa8eol\fP(1), +\fBa8utf8\fP(1), +\fBatr2xfd\fP(1), +\fBatrsize\fP(1), +\fBaxe\fP(1), +\fBblob2c\fP(1), +\fBcart2xex\fP(1), +\fBdasm2atasm\fP(1), +\fBfenders\fP(1), +\fBrom2cart\fP(1), +\fBunmac65\fP(1), +\fBxexcat\fP(1), +\fBxexsplit\fP(1), +\fBxfd2atr\fP(1). +.sp +Any good Atari 8\-bit book: \fIDe Re Atari\fP, \fIThe Atari BASIC Reference +Manual\fP, the \fIOS Users\(aq Guide\fP, \fIMapping the Atari\fP, etc. +.\" Generated by docutils manpage writer. +. diff --git a/xfd2atr.c b/xfd2atr.c new file mode 100644 index 0000000..3fad55e --- /dev/null +++ b/xfd2atr.c @@ -0,0 +1,227 @@ +#include <stdio.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> + +#ifndef VERSION +#define VERSION "???" +#endif + +#define SELF "xfd2atr" + +#define USAGE \ + SELF " v" VERSION " by B. Watson (WTFPL)\n" \ + "Usage: " SELF " -[sd] input.xfd [output.atr]\n" + +int main(int argc, char **argv) { + struct stat st; + char infile[4096], outfile[4096]; + FILE *in, *out; + int i, paras, secsize = 0, seccount; + + if(argc < 2 || argc > 4) { + fprintf(stderr, USAGE); + exit(1); + } + + if(argv[1][0] == '-') { + switch(argv[1][1]) { + case 's': + secsize = 128; + break; + + case 'd': + secsize = 256; + break; + + default: + fprintf(stderr, + SELF ": invalid option -'%c'\n" USAGE, argv[1][1]); + } + + argv++; argc--; + } + + strcpy(infile, argv[1]); + if(argc == 3) { + strcpy(outfile, argv[2]); + } else if(strcmp(infile, "-") == 0) { + strcpy(outfile, "-"); + } else { + char *p; + strcpy(outfile, argv[1]); + + p = strstr(outfile, ".xfd"); + if(!p) p = strstr(outfile, ".XFD"); + if(!p) p = outfile + strlen(outfile); + strcpy(p, ".atr"); + } + + fprintf(stderr, SELF ": input '%s', output '%s'\n", infile, outfile); + + if(strcmp(infile, "-") == 0) { + in = stdin; + } else { + if( !(in = fopen(infile, "rb")) ) { + fprintf(stderr, SELF ": (fatal) can't read %s: %s\n", + infile, strerror(errno)); + exit(1); + } + } + + if(fstat(fileno(in), &st)) { + fprintf(stderr, SELF ": (fatal) can't stat %s: %s\n", + infile, strerror(errno)); + exit(1); + } + + /* A few sanity checks... */ + if(st.st_size < 384) { + fprintf(stderr, + SELF ": (fatal) %s too small to be an XFD image (<384 bytes)\n", + infile); + exit(2); + } + + if(st.st_size % 128 == 16) { + fprintf(stderr, + SELF ": (fatal) %s looks like an ATR image, not an XFD\n", + infile); + exit(2); + } + + if(st.st_size % 128 != 0) { + fprintf(stderr, + SELF ": (fatal) %s not a valid XFD image (not an even number " + "of sectors)\n", infile); + exit(2); + } + + if(st.st_size > (65535 * 256)) { + fprintf(stderr, SELF ": (fatal) %s too large to be an XFD image (>16M)\n", + infile); + exit(2); + } + + if(!secsize) { + char *type; + fprintf(stderr, SELF ": guessing type; use -s or -d to set.\n"); + + /* Automagically figure out the sector size and count */ + if(st.st_size == 720 * 128) { + type = "90K SS/SD image"; + secsize = 128; + } else if(st.st_size == 1040 * 128) { + type = "130K SS/ED image"; + secsize = 128; + } else if(st.st_size == 720 * 256 - 128 * 3) { + type = "180K SS/DD image"; + secsize = 256; + } else if(st.st_size == 1440 * 256 - 128 * 3) { + type = "360K DS/DD image"; + secsize = 256; + } else if(st.st_size < 720 * 128) { + type = "<90K image, assuming SD"; + secsize = 128; + } else if(st.st_size > 720 * 128 && st.st_size < 1040 * 128) { + type = ">90K, <130K image, assuming SD sectors"; + secsize = 128; + } else if(st.st_size % 256 == 0) { + fprintf(stderr, SELF ": Non-standard %dK image, assuming SD sectors\n", + (int)st.st_size / 1024); + type = "Large floppy or hard disk, SD"; + secsize = 128; + } else { + fprintf(stderr, SELF ": Non-standard %dK image, assuming DD sectors\n", + (int)st.st_size / 1024); + type = "Large floppy or hard disk, DD"; + secsize = 256; + } + + fprintf(stderr, SELF ": guessed type: %s\n", type); + } + + if(secsize == 128) { + seccount = st.st_size / secsize; + if(seccount & 1) + fprintf(stderr, SELF ": odd number of sectors in SD image, might " + "actually be DD (try with -d?)\n"); + } else { + seccount = (st.st_size - 384) / secsize + 3; + if(st.st_size % 256 != 128) + fprintf(stderr, SELF ": partial sector at end of DD image, might " + "actually be SD (try with -s?)\n"); + } + + /* One last sanity check */ + if(seccount > 65535) { + fprintf(stderr, + SELF ": (fatal) %s too large to be an XFD image " + "at current density (>65535 sectors)\n", + infile); + if(secsize == 128) + fprintf(stderr, SELF ": Try forcing double density with -d\n"); + exit(2); + } + + fprintf(stderr, SELF ": sectors: %d, sector size: %d bytes", + seccount, secsize); + if(secsize == 256) + fprintf(stderr, " (first 3 sectors are 128 bytes)"); + fputc('\n', stderr); + + paras = st.st_size / 16; + fprintf(stderr, SELF ": %d 16-byte paragraphs\n", paras); + + /* Only open the output file after the XFD is known to be good */ + if(strcmp(outfile, "-") == 0) { + out = stdout; + } else { + if( !(out = fopen(outfile, "wb")) ) { + fprintf(stderr, SELF ": (fatal) can't write %s: %s\n", + outfile, strerror(errno)); + exit(1); + } + } + + /* output ATR header: */ + fputc(0x96, out); /* NICKATARI cksum, lo byte */ + fputc(0x02, out); /* NICKATARI cksum, hi byte */ + fputc(paras & 0xff, out); /* paragraphs, lo byte */ + fputc((paras >> 8) & 0xff, out); /* paragraphs, mid byte */ + fputc(secsize & 0xff, out); /* sector size, lo byte */ + fputc(secsize >> 8, out); /* sector size, hi byte */ + fputc((paras >> 16) & 0xff, out); /* paragraphs, hi byte */ + + /* unused ATR header bytes */ + for(i=0; i<9; i++) + fputc('\0', out); + + /* copy the data */ + while( (i = fgetc(in)) != EOF ) + fputc(i, out); + + /* fgetc() returns EOF on error *or* EOF; + check for I/O errors, return 1 if so */ + i = 0; + + if(ferror(in)) { + i = 1; + fprintf(stderr, + SELF ": error reading %s: %s\n", infile, strerror(errno)); + } + + if(ferror(out)) { + i = 1; + fprintf(stderr, + SELF ": error writing %s: %s\n", outfile, strerror(errno)); + } + + fclose(in); + fclose(out); + + return i; +} diff --git a/xfd2atr.rst b/xfd2atr.rst new file mode 100644 index 0000000..f28b19c --- /dev/null +++ b/xfd2atr.rst @@ -0,0 +1,82 @@ +.. RST source for xfd2atr(1) man page. Convert with: +.. rst2man.py xfd2atr.rst > xfd2atr.1 +.. rst2man.py comes from the SBo development/docutils package. + +======= +xfd2atr +======= + +------------------------------------------------------------ +Convert an Atari 8-bit XFD (raw) disk image to an ATR image. +------------------------------------------------------------ + +.. include:: manhdr.rst + +SYNOPSIS +======== + +*xfd2atr* [*-sd*] *infile.xfd* [*outfile.atr*] + +DESCRIPTION +=========== + +**xfd2atr** generates and adds a 16-byte ATR header to an XFD +image. If no **-s** or **-d** options are given, xfd2atr tries to +guess the density based on the file size. + +OPTIONS +======= + +-s + Assume the image uses single density (128-byte) sectors, instead + of trying to guess the density from the file size. + +-d + Assume the image uses double density (256-byte) sectors, instead + of trying to guess the density from the file size. + +NOTES +===== + +You may use **-** for *infile* to read from standard input and/or +**-** for *outfile* to write to standard output. If a filename is +supplied for *outfile*, it will always be used as-is (no *.atr* +extension will be appended). + +If *outfile* is omitted, it is constructed like so: + + - If reading from standard input, write to standard output. + + - If reading from a file whose name ends with an *.xfd* or *.XFD* + extension, replace the extension with *.atr*. + + - Otherwise, append *.atr* to the input filename. + +Since XFD images are raw dumps with no header or structure, it's +impossible to know the correct density (bytes/sector) for a given +image for certain. + +However, no known Atari-compatible disk format uses other than 128 +or 256 bytes per sector (or possibly 512, for some hard disk images, +but **xfd2atr** doesn't support these). This means file that isn't a +multiple of 128 bytes in size will be rejected. + +Likewise, no known format uses an odd number of sectors, and +it's assumed that all double-density images will begin with 3 +single-density boot sectors (true of all floppy images you're +ever likely to run across; may not be true of hard disk images). + +Given these assumptions, **xfd2atr** is able to make an educated +guess about the correct sector size and count to use for the ATR +header it generates. If it guesses wrong, the resulting ATR image +will be unusable; if this happens, re-run **xfd2atr** and force the +density with **-s** or **-d**. + +EXIT STATUS +=========== + +Exit status is zero for success, non-zero for failure. Further, +exit status will be 1 for errors involving file I/O (file not found, +permissions, etc), and 2 for structural errors in the XFD file. + +.. include:: manftr.rst |