#!/bin/bash # co2do.sh # by hackerb9, February 2026 # # Given a .CO machine language file for a TRS-80 Model 100 (or # similar), creates a .DO file containing a two-stage BASIC loader # which will install the .CO to the correct address and start it. # This program uses a small assembly routine to decode and write the # bytes to memory, which is a hundred times faster than doing it in # BASIC. (For details see decode.asm and embedasm which writes the # decode() function below.) # # USAGE: co2do.sh FOO.CO FOO.DO # Transfer FOO.DO to M100. # On M100: run "FOO" # # Features: # # * BASIC loader writes .CO to memory very quickly (less than a second). # # * Inspired by Stephen Adolph's efficient encoding scheme which # increases storage size by at most 2x + k (where k is approx. 500), # and on average closer to 1.3x + k. # # * Works on all of the Kyotronic Sisters: # Kyocera Kyotronic 85, TRS-80 Model 100/102, Tandy 200, Olivetti M10, # NEC PC-8201/8201A/8300. # # * BASIC Automatically CLEARs the correct space and CALLs the program. # # * Uses .CO header to detect where to POKE, length mismatch, and CALL addr. # # * As a special bonus, if you use the -t option, it will display a # Unicode version of the program instead of writing to a .DO file. # (Requires the tandy-200.charmap file from hackerb9/tandy-locale.) # Todo: # * Prevent writing to bad parts of RAM. # (E.g., POKE Q where Q=MAXRAM). # set +o errexit set +o pipefail function usage() { cat < [OUTPUT.DO] EOF } function main() { declare -i TOP LEN EXE declare -i ROT=136 # Rotate character set for efficiency TOP=$1+$2*256 LEN=$3+$4*256 EXE=$5+$6*256 shift 6 if [[ ! $quiet ]]; then cat<<-EOF >&2 TOP: $TOP END: $((TOP+LEN)) EXE: $EXE ROT: $ROT EOF fi if [[ $# -ne $LEN ]]; then echo "Warning filesize mismatch! $# bytes found after header, not $LEN." >&2 if [[ $# -gt $LEN ]]; then echo "Truncating output to $LEN bytes." >&2 set -- "${@:1:LEN}" fi fi if (( TOP<62960 && TOP>32767 )); then clear=", $TOP"; else clear=""; fi # We'll be POKEing into and executing the fast M/L decoder # directly out of the space allocated for DC$, so we need to make # sure it doesn't move around in memory as BASIC is wont to do. cat <<-EOF 10 MAXFILES=0: CLEAR 256$clear: NM$="${coname}" 20 TP=$((TOP)): LN=$LEN: EX=$((EXE)): RT=$((ROT)): ID=PEEK(1) 30 DC$=SPACE\$(255) 40 GOSUB 12000: GOSUB 13000 50 IF FRE(0) < LN THEN ?"Insufficient space to save "NM$: Q$=CHR\$(34): C$=",":?"Please try NEW then SAVEM "Q$;NM$;Q$;C$;TP;C$;TP+LN;C$;EX: END 60 ?"Saving "NM$;:IF ID<>148 THEN SAVEM NM$,TP,TP+LN,EX ELSE BSAVE NM$,TP,LN,EX 80 ?:?"Running "NM$:IF ID<>148 THEN CALL EX ELSE EXEC EX 90 END EOF emitmldecode emitbasicdecode printf "14000 '${coname}" emitdata "$@" emitnecvarptr true # XXX todo: call external warnmem util return # Return result from warnmem } function emitdata() { local -i linenum=14000 local -i v for v; do v=(v+$ROT)%256 if (( count++ % 120 == 0 )); then printf '\n%d DATA"' $((linenum+=10)) fi # Escape Quote (34), Bang (33), DEL (127), and ctrl chars (except Tab) if (( (v<35 && v!=9 && v!=32) || v==127 )); then v=v+128 printf "!" fi printf -v x "%x" $v printf "\x$x" done # End of DATA marked by '!!' printf '\n%d DATA !!\n' $((linenum+=10)) } function emitmldecode() { # BASIC wrapper for the M/L routine to decode bang-code DATA strings. # Sets the BASIC variable DX to the address of the routine. # (See routine starting at lines 13000 for example usage.) # # NOTA BENE: DX is actually the address of DC$'s buffer. The # machine language is POKEd directly into and executed from the # string, so manipulation of other string variables in BASIC can # foul it up. (READ is okay as it merely sets variables to point # to the program line.) # USAGE: Initialize by calling DX with A set to the rotation # (charset offset) and HL set to the destination address for the # decoded data. In a loop, read encoded data into P$ and call DX+9 # with HL set to the VARPTR(P$). Re-initialization is not # necessary for each P$; each call to DX+9 appends data to the # destination. While probably not useful, at the end of each call # the length of P$ is set to the number of decoded bytes written. cat <<-EOF 12000 'Fast loader 12010 IF PEEK(1)<>148 THEN DC=VARPTR(DC$): ELSE VY$="DC$": GOSUB 40000: DC=L+256*H 12015 DX=PEEK(DC+1)+256*PEEK(DC+2): Q=DX 12020 READ X 12030 IF X=-1 THEN 12080 12040 IF X=-7 THEN READ L: READ H: A=H*256+L+DX: X=INT(A/256): L=A-X*256: POKE Q, L: Q=Q+1 12050 POKE Q, X 12060 Q=Q+1 12070 GOTO 12020 12080 POKE DC, Q-DX: 'Set length of DC$ 12090 RETURN EOF decode } function emitbasicdecode() { # Calls M/L routine to decode and install each DATA string. # DX is the start address of decode.asm, set by emitmlcode(). # RT is the amount of rotation for the character set. # TP is the destination address ("TOP"), set by main(). cat <<-"EOF" 13000 REM ?"Loading "NM$" ("LN" bytes) at "TP 13020 IF PEEK(1)<>148 THEN CALL DX, RT, TP: ELSE H=INT(TP/256): L=TP-H*256: POKE 63911, RT: POKE 63912, L: POKE 63913, H: EXEC DX 13030 READP$: IF P$="!!" GOTO 13080 13040 IF PEEK(1)<>148 THEN P=VARPTR(P$): CALL DX+9, 0, P: ELSE VY$="P$": GOSUB 40030: POKE 63912, L: POKE 63913, H: EXEC DX+9 13060 GOTO 13030 13080 'All done 13090 ?:RETURN EOF } emitnecvarptr() { # VARPTR for NEC 8201/8300 by Gary Weber of web8201.net # Entry: VY$ must contain the name of variable of interest # Exit: H & L contain variable's address, TY contains type # Example: # 100 A$="This is a sample string." # 110 VY$="A$":GOSUB 40000 # 120 PRINT "VARPTR of ";VY$;" is ";L+(H*256) # NOTES From hackerb9: # * Minor modifications to use less RAM and string space. --b9 2026 # * M/L routine placed at 64448 for performing VARPTR. # * Address 64457 is an 8-byte, NULL terminated copy of VY$. # * This routine passes 64457 in HL when it EXECs 64448. # * The result is read out of HL (addresses 63912,3) and A (63911). cat <<-EOF 39999 'VARPTR by Gary Weber for NEC 8201/8300 40000 A=64448 40010 POKEA,205:POKEA+1,175:POKEA+2,73:POKEA+3,235 40020 POKEA+4,58:POKEA+5,139:POKEA+6,250:POKEA+8,201 40030 IFVY$=""THENPRINT"VY$ not defined!":STOP 40035 A=64457 ' HL register for EXEC = 201/251 40040 FORH=1TOLEN(VY$):POKEA,ASC(MID\$(VY$,H,1)):A=A+1:NEXT:POKEA,0:POKE64464,0 40050 POKE63912,201:POKE63913,251:EXEC64448 40060 L=PEEK(63912):H=PEEK(63913):TY=PEEK(63911) 40070 RETURN EOF } ### BEGIN AUTOMATIC EMBED ### decode.asm.lst 2026-03-22 09:54:33 #### DO NOT EDIT! Added automatically by embedasm. decode() { cat <<-'EOT' 12500 DATA 50,-7,42,0,34,-7,7,0,201,0,0,229,70,14,0,35,94,35,86,42,-7,7,0,235,120,167,202,-7,52,0,126,254,33,194,-7,40,0,5,202,-7,52,0,35,126,238,128,235,214,0,119,235,35,19,12,5,194,-7,26,0,235,34,-7,7,0,235,225,113,201,-1 EOT } ### END AUTOMATIC EMBED ### { # CLI args tandycharset=cat quiet="" while getopts 'qt' name; do case $name in q) quiet=: ;; t) tandycharset="iconv -f $(dirname $0)/tandy-200.charmap" ;; *) exit 1 ;; esac done shift $(($OPTIND - 1)) if [ -z "$1" ]; then usage; exit 1; fi if [[ "$1" == "-" ]]; then shift; set -- /dev/stdin "$@"; fi if [ ! -r "$1" ]; then echo "'$1' is not readable" exit 1 else input="$1" fi output=/dev/stdout if [ "$2" ]; then output="$2" shift elif [[ $tandycharset == "cat" ]]; then if [[ $input == *.CO ]]; then output=${input%.CO}.DO else output=${input%.*}.do fi fi coname=$(basename "$output") coname=${coname%%.*} coname=${coname:0:6} coname=${coname@U}.CO } outputfilter() { # Add carriage returns since NEC PC-8201 uses Mac (or DOS?) line endings. # Append ^Z at end of file to signal M100 that file is finished. sed 's/$/\r/g' printf '\x1A' } main $(od -t u1 -v -An "$input") | $tandycharset | outputfilter > "$output" err=${PIPESTATUS[0]} if (( err != 0 )); then echo "Exiting with error $err" >&2 exit $err fi if [[ $tandycharset == "cat" && -z $quiet ]]; then outnodo=${output##*/} outnodo=${outnodo%.[Dd][Oo]} cat <<-EOF Now transfer $output to your Model-T and in BASIC type run "${outnodo:0:6}" EOF fi exit $err