From e2ba8458a5cfdfacfaf103e7ba97d610afa6c970 Mon Sep 17 00:00:00 2001 From: "B. Watson" Date: Mon, 29 Aug 2022 16:11:13 -0400 Subject: initial commit --- .syms | 0 Makefile | 166 ++++++ README | 79 +++ TODO | 19 + a8eol.1 | 482 ++++++++++++++++++ a8eol.c | 567 +++++++++++++++++++++ a8eol.rst | 196 +++++++ a8utf8 | 163 ++++++ a8utf8.1 | 108 ++++ a8utf8.rst | 52 ++ asmwrapper.sh | 35 ++ atr2xfd.1 | 201 ++++++++ atr2xfd.c | 244 +++++++++ atr2xfd.rst | 109 ++++ atrsize.1 | 216 ++++++++ atrsize.c | 308 +++++++++++ atrsize.rst | 92 ++++ axe.1 | 166 ++++++ axe.c | 197 +++++++ axe.h | 52 ++ axe.rst | 114 +++++ axelib.c | 546 ++++++++++++++++++++ blob2c.1 | 137 +++++ blob2c.c | 117 +++++ blob2c.rst | 85 ++++ cart.c | 261 ++++++++++ cart.h | 55 ++ cart2xex.1 | 247 +++++++++ cart2xex.c | 583 +++++++++++++++++++++ cart2xex.rst | 203 ++++++++ dasm2atasm | 275 ++++++++++ dasm2atasm.1 | 244 +++++++++ dasm2atasm.rst | 162 ++++++ equates.inc | 1384 ++++++++++++++++++++++++++++++++++++++++++++++++++ fenders.1 | 282 ++++++++++ fenders.bin | Bin 0 -> 384 bytes fenders.c | 418 +++++++++++++++ fenders.dasm | 570 +++++++++++++++++++++ fenders.rst | 239 +++++++++ fenders_bin.c | 54 ++ fenders_bin.h | 9 + fenders_offsets.h | 5 + fenders_offsets.pl | 6 + fendersdbl.bin | Bin 0 -> 640 bytes fendersdbl.dasm | 411 +++++++++++++++ fendersdbl_bin.c | 86 ++++ fendersdbl_bin.h | 9 + fendersdbl_offsets.h | 5 + get_address.c | 22 + get_address.h | 2 + loadscreen.bin | Bin 0 -> 222 bytes loadscreen.dasm | 153 ++++++ loadscreen_bin.c | 34 ++ loadscreen_bin.h | 9 + manftr.rst | 30 ++ manhdr.rst | 7 + rom2cart.1 | 244 +++++++++ rom2cart.c | 641 +++++++++++++++++++++++ rom2cart.rst | 202 ++++++++ rstman.rst | 58 +++ test2 | 2 + testdata | 1 + testdata.orig | 1 + unmac65.1 | 392 ++++++++++++++ unmac65.c | 1041 +++++++++++++++++++++++++++++++++++++ unmac65.rst | 324 ++++++++++++ ver.rst | 1 + xex.c | 313 ++++++++++++ xex.h | 117 +++++ xexcat.1 | 181 +++++++ xexcat.c | 248 +++++++++ xexcat.rst | 132 +++++ xexsplit.1 | 195 +++++++ xexsplit.c | 123 +++++ xexsplit.rst | 128 +++++ xextest.c | 87 ++++ xfd2atr.1 | 132 +++++ xfd2atr.c | 227 +++++++++ xfd2atr.rst | 82 +++ 79 files changed, 15088 insertions(+) create mode 100644 .syms create mode 100644 Makefile create mode 100644 README create mode 100644 TODO create mode 100644 a8eol.1 create mode 100644 a8eol.c create mode 100644 a8eol.rst create mode 100755 a8utf8 create mode 100644 a8utf8.1 create mode 100644 a8utf8.rst create mode 100644 asmwrapper.sh create mode 100644 atr2xfd.1 create mode 100644 atr2xfd.c create mode 100644 atr2xfd.rst create mode 100644 atrsize.1 create mode 100644 atrsize.c create mode 100644 atrsize.rst create mode 100644 axe.1 create mode 100644 axe.c create mode 100644 axe.h create mode 100644 axe.rst create mode 100644 axelib.c create mode 100644 blob2c.1 create mode 100644 blob2c.c create mode 100644 blob2c.rst create mode 100644 cart.c create mode 100644 cart.h create mode 100644 cart2xex.1 create mode 100644 cart2xex.c create mode 100644 cart2xex.rst create mode 100755 dasm2atasm create mode 100644 dasm2atasm.1 create mode 100644 dasm2atasm.rst create mode 100644 equates.inc create mode 100644 fenders.1 create mode 100644 fenders.bin create mode 100644 fenders.c create mode 100644 fenders.dasm create mode 100644 fenders.rst create mode 100644 fenders_bin.c create mode 100644 fenders_bin.h create mode 100644 fenders_offsets.h create mode 100644 fenders_offsets.pl create mode 100644 fendersdbl.bin create mode 100644 fendersdbl.dasm create mode 100644 fendersdbl_bin.c create mode 100644 fendersdbl_bin.h create mode 100644 fendersdbl_offsets.h create mode 100644 get_address.c create mode 100644 get_address.h create mode 100644 loadscreen.bin create mode 100644 loadscreen.dasm create mode 100644 loadscreen_bin.c create mode 100644 loadscreen_bin.h create mode 100644 manftr.rst create mode 100644 manhdr.rst create mode 100644 rom2cart.1 create mode 100644 rom2cart.c create mode 100644 rom2cart.rst create mode 100644 rstman.rst create mode 100644 test2 create mode 100644 testdata create mode 100644 testdata.orig create mode 100644 unmac65.1 create mode 100644 unmac65.c create mode 100644 unmac65.rst create mode 100644 ver.rst create mode 100644 xex.c create mode 100644 xex.h create mode 100644 xexcat.1 create mode 100644 xexcat.c create mode 100644 xexcat.rst create mode 100644 xexsplit.1 create mode 100644 xexsplit.c create mode 100644 xexsplit.rst create mode 100644 xextest.c create mode 100644 xfd2atr.1 create mode 100644 xfd2atr.c create mode 100644 xfd2atr.rst diff --git a/.syms b/.syms new file mode 100644 index 0000000..e69de29 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) diff --git a/README b/README new file mode 100644 index 0000000..e993246 --- /dev/null +++ b/README @@ -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 on irc.libera.chat ##atari. diff --git a/TODO b/TODO new file mode 100644 index 0000000..b40a732 --- /dev/null +++ b/TODO @@ -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). diff --git a/a8eol.1 b/a8eol.1 new file mode 100644 index 0000000..daf4993 --- /dev/null +++ b/a8eol.1 @@ -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. +. diff --git a/a8eol.c b/a8eol.c new file mode 100644 index 0000000..6d6b779 --- /dev/null +++ b/a8eol.c @@ -0,0 +1,567 @@ +#include +#include +#include +#include +#include + +#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 + diff --git a/a8utf8 b/a8utf8 new file mode 100755 index 0000000..f5f2df7 --- /dev/null +++ b/a8utf8 @@ -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 < 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 +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include + +/* 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 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 diff --git a/axe.1 b/axe.1 new file mode 100644 index 0000000..f26293b --- /dev/null +++ b/axe.1 @@ -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. +. diff --git a/axe.c b/axe.c new file mode 100644 index 0000000..838a73c --- /dev/null +++ b/axe.c @@ -0,0 +1,197 @@ +/* axe - manipulate atari disk images (dos 2.0s only for now) + * + * Usage: ataridir [-a] [-x filename] [-l] [-v] + * + * (-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); +} diff --git a/axe.h b/axe.h new file mode 100644 index 0000000..1301d0c --- /dev/null +++ b/axe.h @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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); diff --git a/axe.rst b/axe.rst new file mode 100644 index 0000000..01e8a10 --- /dev/null +++ b/axe.rst @@ -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 +#include +#include +#include +#include +#include + +/* 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 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 diff --git a/cart.c b/cart.c new file mode 100644 index 0000000..09c1c12 --- /dev/null +++ b/cart.c @@ -0,0 +1,261 @@ +/* CART header support library */ + +#include +#include +#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); +} + diff --git a/cart.h b/cart.h new file mode 100644 index 0000000..f2b10a4 --- /dev/null +++ b/cart.h @@ -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 +#include +#include +#include +#include +#include + +#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= 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= 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 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 </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 = <$out" or die "Can't write to $out: $!\n"; + +$hdr = <) { + 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 +.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 + + **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\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 new file mode 100644 index 0000000..ee8146c Binary files /dev/null and b/fenders.bin differ 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 +#include +#include +#include +#include +#include + +#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= 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 ; 8 . + STA SDLSTL+1 + + ; set up to start reading the directory + 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 ; "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 ; ".....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 ** 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 new file mode 100644 index 0000000..94c4545 Binary files /dev/null and b/fendersdbl.bin differ 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 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 ; 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 ; 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 + .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 +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 + +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 new file mode 100644 index 0000000..7ae3e00 Binary files /dev/null and b/loadscreen.bin differ 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 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 + 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 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 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 +#include +#include +#include +#include +#include + +#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 diff --git a/test2 b/test2 new file mode 100644 index 0000000..00ef866 --- /dev/null +++ b/test2 @@ -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 +#include +#include +#include +#include + +#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 diff --git a/ver.rst b/ver.rst new file mode 100644 index 0000000..6f54bd4 --- /dev/null +++ b/ver.rst @@ -0,0 +1 @@ +.. |version| replace:: 0.2.1 diff --git a/xex.c b/xex.c new file mode 100644 index 0000000..e4b66ce --- /dev/null +++ b/xex.c @@ -0,0 +1,313 @@ +#include +#include +#include + +#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)); +} diff --git a/xex.h b/xex.h new file mode 100644 index 0000000..2300481 --- /dev/null +++ b/xex.h @@ -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 +#include +#include +#include +#include + +#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 +#include +#include +#include +#include + +#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 +#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 +#include +#include +#include +#include +#include +#include + +#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 -- cgit v1.2.3