Monday, January 27, 2020

Micro Chroma 68 Tape Storage

In my previous post, I showed video of me loading and running code on an emulated Micro Chroma 68 (aka uchroma68) machine, and hinted at a bigger project. I intended to be back sooner to describe the code loading process and then move-on to the bigger project. But, I got started on the bigger project and neglected the next blog update -- time to get back on track!

Audio-Encoded Data

Code can be loaded onto the uchroma68 in a variety of ways, including from the keyboard input or over an audio-encoded interface intended for using "compact cassettes" for mass storage. For real hardware, other options include modifications to use an ACIA for serial input or modern interfaces to drive the keyboard interface from a remote computer. But since our objective includes targeting the unmodified MAME-emulated uchroma68, it seems best to pursue only standard options for loading code.

So, how to convert object coded into appropriate audio?

Kansas City Standard

The Kansas City standard (KCS), or Byte standard, is a way of storing digital data on standard Compact Audio Cassettes at data rates of 300 to 2400 bits per second (at 300–2400 baud) that was first defined in 1976. It originated in a symposium sponsored by Byte magazine in November 1975 in Kansas City, Missouri to develop a standard for storage of digital microcomputer data on inexpensive consumer quality cassettes.
The uchroma68 uses the Kansas City Standard for audio data encoding. This old standard defined how to transfer bits and bytes as audio, suitable for storage on consumer audio tape. Fortunately for me, open source tools (e.g. py-kcs) already exist for encoding and decoding such audio. Unfortunately, some of those tools presume to deal only with ASCII-encoded test files (e.g. BASIC programs). Since I needed to deal with binary data, I did have to write a patch to allow for handling binary input to the KCS encoder:
 diff --git a/kcs_encode.py b/kcs_encode.py
index 21bbf080917d..1f078dd0b537 100755
--- a/kcs_encode.py
+++ b/kcs_encode.py
@@ -52,7 +52,7 @@ def kcs_encode_byte(byteval):

 # Write a WAV file with encoded data. leader and trailer specify the
 # number of seconds of carrier signal to encode before and after the data
-def kcs_write_wav(filename,data,leader,trailer):
+def kcs_write_wav(filename,data,leader,trailer,binary):
     w = wave.open(filename,"wb")
     w.setnchannels(1)
     w.setsampwidth(1)
@@ -64,7 +64,7 @@ def kcs_write_wav(filename,data,leader,trailer):
     # Encode the actual data
     for byteval in data:
         w.writeframes(kcs_encode_byte(byteval))
-        if byteval == 0x0d:
+        if not binary and byteval == 0x0d:
             # If CR, emit a short pause (10 NULL bytes)
             w.writeframes(null_pulse)
   
@@ -74,16 +74,29 @@ def kcs_write_wav(filename,data,leader,trailer):

 if __name__ == '__main__':
     import sys
-    if len(sys.argv) != 3:
-        print("Usage : %s infile outfile" % sys.argv[0],file=sys.stderr)
+    import optparse
+
+    p = optparse.OptionParser()
+    p.add_option("-b",action="store_true",dest="binary")
+    p.add_option("--binary",action="store_true",dest="binary")
+    p.set_defaults(binary=False)
+
+    opts, args = p.parse_args()
+    if len(args) != 2:
+        print("Usage : %s [-b] infile outfile" % sys.argv[0],file=sys.stderr)
         raise SystemExit(1)

-    in_filename = sys.argv[1]
-    out_filename = sys.argv[2]
-    data = open(in_filename,"U").read()
-    data = data.replace('\n','\r\n')         # Fix line endings
-    rawdata = bytearray(data.encode('latin-1'))
-    kcs_write_wav(out_filename,rawdata,5,5)
+    in_filename = args[0]
+    out_filename = args[1]
+    if opts.binary:
+        data = open(in_filename, 'rb').read()
+        rawdata = bytearray(data)
+    else:
+        data = open(in_filename, 'r', newline=None).read()
+        data = data.replace('\n','\r\n')         # Fix line endings
+        rawdata = bytearray(data.encode('latin-1'))
+
+    kcs_write_wav(out_filename,rawdata,5,5,opts.binary)
With that, I could encode audio data for the uchroma68 by using the '-b' option which I added to kcs_encode.py. But what about the logical format of that data?

JBUG-Compatible Format

Knowing how to send 0's and 1's is not too helpful until one knows what 0's and 1's to send. Even a program binary still needs to be accompanied by information such as where to write the program in memory and perhaps protocol data used to manage the transfer. The TVBUG firmware on the uchroma68 uses a format for binary data that is compatible with the format used by the JBUG firmware from the MEK6800D2 board which had preceded it, although TVBUG allows for an extension to the format that provides for the program name to be encoded along with the program data. Based on information from the TVBUG and JBUG manuals and some reading of the assembly language listings of those programs, I was able to write a script that converts a binary program file into a hex dump representing the data prepared for audio encoding:
#!/bin/sh

###########################################################
# Convert a binary to hexdump of JBUG logical tape format
#       (used by both JBUG and TVBUG)
###########################################################

usage() {
        cat <<USAGE
usage: $(basename $0) [-l <loadaddr>] [-n <progname>] [-p] <filename>

        -l <loadaddr>   base load address
        -n <progname>   output program name
        -p              pad tape output with 0xff characters
        <filename>      input data file
USAGE
}

while getopts ":l:n:p" opt
do
        case ${opt} in
        l)
                LOADADDR=$OPTARG
                ;;
        n)
                PROGNAME=$OPTARG
                ;;
        p)
                PADOUT=1
                ;;
        \?)
                echo "Invalid option: $OPTARG" 1>&2
                ;;
        :)
                echo "Invalid option: $OPTARG requires an argument" 1>&2
                ;;
        esac
done
shift $((OPTIND-1))

# Set default load address of zero (if not specified on command-line)
LOADADDR=${LOADADDR:-0}

if [ $# -ne 1 ]
then
        usage
        exit 1
fi

INFILE=$1
INSIZE=$(stat -c %s $INFILE)

if [ x$PADOUT = x1 ]
then
        for i in $(seq 1 1024)
        do
                echo -n 'ff'
        done
        echo
fi

if [ -n "$PROGNAME" ]
then
        echo -n $PROGNAME | xxd -p
        echo '0d'
fi

OUTSIZE=0
while [ $OUTSIZE -lt $INSIZE ]
do
        BLOCKSIZE=$(($INSIZE - $OUTSIZE))
        if [ $BLOCKSIZE -gt 256 ]
        then
                BLOCKSIZE=256
        fi

        echo -n 'ff'
        echo -n '42'
        printf '%x' $(($BLOCKSIZE - 1))
        printf '%04x' $(($LOADADDR + $OUTSIZE))

        xxd -p -g 1 -l $BLOCKSIZE -s $OUTSIZE $INFILE

        OUTSIZE=$(($OUTSIZE + $BLOCKSIZE))
done

if [ x$PADOUT = x1 ]
then
        for i in $(seq 1 144)
        do
                echo -n 'ff'
        done
fi

echo -n 'ff'
echo '47'
The hex dump output can be fed to the xxd utility (e.g. 'xxd -p -r'), captured, and then encoded to Kansas City Standard format audio.

Demo

In my earlier post, I mentioned "a text-based animation that I originally ported from a VZ-200 C program to assembly language for the MC-10 which I had always suspected would be easy to get on the uchroma68". Well, I was right -- changing two "equ" statements (one for the load address and one for the screen address) was all that was required...


But wait -- there's more! The bigger project involves more porting from the MC-10 to the Micro Chroma 68, this time porting some code that originated on the Tandy Color Computer before being ported by Darren Atkinson.

Intrigued? Then stay tuned...

No comments:

Post a Comment