Author Archives: Allen Huffman

About Allen Huffman

Co-founder of Sub-Etha Software.

CoCo Disk BASIC disk structure – part 4

I did a thing.

I wrote a BASIC program which will create 68 files named “0.TXT” to “67.TXT”. Each file is 2304 bytes so it takes up a full granule. (That is not really important. It just helps makes things obvious if you look at the disk with a hex editor and want to know which file is at which sector.)

After making these files, it uses code from some of my other examples to scan through the directory and display it (FILEGRAN.BAS code, shown later in this post) and then it scans the directory and prints which granule each 1-gran file is using.

I can start with a freshly formatted disk then run this program and see where RS-DOS put each file.

Will it match the order RS-DOS used when making one huge file that takes up all 68 grans? Let’s find out…

10 '68FILES.BAS
20 PRINT "RUN THIS ON A BLANK DISK."
30 INPUT "DRIVE #";DR
40 'SWITCH TO THAT DRIVE
50 DRIVE DR
60 'GOTO 140
70 'MAKE FILES 0-67
80 FOR G=0 TO 67
90 F$=MID$(STR$(G),2)+".TXT"
100 PRINT "MAKING ";F$;
110 OPEN "O",#1,F$
120 CLOSE #1:PRINT
130 NEXT
140 'FILEGRAN.BAS
150 'DIR WITHOUT FILE SIZES
160 CLEAR 512:DIM SP$(1)
170 ' S - SECTOR NUMBER
180 FOR S=3 TO 11
190 ' SP$(0-1) - SECTOR PARTS
200 DSKI$ DR,17,S,SP$(0),SP$(1)
210 ' P - PART OF SECTOR
220 FOR P=0 TO 1
230 ' E - DIR ENTRY (4 P/SECT.)
240 FOR E=0 TO 3
250 ' GET 32 BYTE DIR ENTRY
260 DE$=MID$(SP$(P),1+E*32,32)
270 ' FB - FIRST BYTE OF NAME
280 FB=ASC(LEFT$(DE$,1))
290 ' SKIP DELETED FILES
300 IF FB=0 THEN 440
310 ' WHEN 255, DIR IS DONE
320 IF FB=255 THEN 470
330 ' PRINT NAME AND EXT.
340 'PRINT LEFT$(DE$,8);TAB(9);MID$(DE$,9,3);
350 ' FIRST TWO CHARS ONLY
360 PRINT LEFT$(DE$,2);"-";
361 'PRINT #-2,LEFT$(DE$,2);",";
370 ' FILE TYPE
380 'PRINT TAB(13);ASC(MID$(DE$,12,1));
390 ' BINARY OR ASCII
400 'IF ASC(MID$(DE$,13,1))=0 THEN PRINT "B"; ELSE PRINT "A";
410 ' STARTING GRANULE
420 PRINT USING("## ");ASC(MID$(DE$,14,1));
421 'PRINT #-2,ASC(MID$(DE$,14,1))
430 CL=CL+1:IF CL=5 THEN CL=0:PRINT
440 NEXT
450 NEXT
460 NEXT
470 END

I modified this program to output to the printer (PRINT #-2) and then capture that output in the Xroar emulator in a text file. That gave me data which I put in a spreadsheet.

 68 Files
FILE GRAN
0 32
1 33
2 34
3 35
4 30
5 31
6 36
7 37
8 28
9 29
10 38
11 39
12 26
13 27
14 40
15 41
16 24
17 25
18 42
19 43
20 22
21 23
22 44
23 45
24 20
25 21
26 46
27 47
28 18
29 19
30 48
31 49
32 16
33 17
34 50
35 51
36 14
37 15
38 52
39 53
40 12
41 13
42 54
43 55
44 10
45 11
46 56
47 57
48 8
49 9
50 58
51 59
52 6
53 7
54 60
55 61
56 4
57 5
58 62
59 63
60 2
61 3
62 64
63 65
64 0
65 1
66 66
67 67

Next, I used a second program on a freshly formatted disk to create one big file fully filling up the disk. (The very last PRINT to the file will create a ?DF ERROR, which I now think is a bug. It should not do that until I try to write the next byte, I think.)

10 '1BIGFILE.BAS
20 PRINT"RUN THIS ON A BLANK DISK."
30 INPUT "DRIVE #";DR
40 'SWITCH TO THAT DRIVE
50 DRIVE DR
60 'MAKE ONE BIG 68 GRAN FILE
70 OPEN "O",#1,"1BIGFILE.TXT"
80 FOR G=0 TO 67
90 PRINT G;
100 T$=STRING$(128,G)
110 FOR T=1 TO 18
120 PRINT ".";
130 PRINT #1,T$;
140 NEXT
150 PRINT
160 NEXT
170 CLOSE #1
180 END

I ran another test program which would read the directory, then print out the granule chain of each file on the disk.

10 ' FILEGRAN.BAS
20 '
30 ' 0.0 2025-11-20 BASED ON FILEINFO.BAS
40 '
50 ' E$(0-1) - SECTOR HALVES
60 ' FT$ - FILE TYPE STRINGS
70 '
80 CLEAR 1500:DIM E$(1),FT$(3)
90 FT$(0)="BPRG":FT$(1)="BDAT":FT$(2)="M/L ":FT$(3)="TEXT "
100 '
110 ' DIR HOLDS UP TO 72 ENTRIES
120 '
130 ' NM$ - NAME
140 ' EX$ - EXTENSION
150 ' FT - FILE TYPE (0-3)
160 ' AF - ASCII FLAG (0/255)
170 ' FG - FIRST GRANULE #
180 ' BU - BYTES USED IN LAST SECTOR
190 ' SZ - FILE SIZE
200 ' GM - GRANULE MAP
210 '
220 DIM NM$(71),EX$(71),FT(71),AF(71),FG(71),BU(71),SZ(71),GM(67)
230 '
240 INPUT "DRIVE";DR
250 '
260 ' FILE ALLOCATION TABLE
270 ' 68 GRANULE ENTRIES
280 '
290 DIM FA(67)
300 DSKI$ DR,17,2,G$,Z$:Z$=""
310 FOR G=0 TO 67
320 FA(G)=ASC(MID$(G$,G+1,1))
330 NEXT
340 '
350 ' READ DIRECTORY
360 '
370 DE=0
380 FOR S=3 TO 11
390 DSKI$ DR,17,S,E$(0),E$(1)
400 '
410 ' PART OF SECTOR
420 '
430 FOR P=0 TO 1
440 '
450 ' ENTRY WITHIN SECTOR PART
460 '
470 FOR E=0 TO 3
480 '
490 ' DIR ENTRY IS 32 BYTES
500 '
510 E$=MID$(E$(P),E*32+1,32)
520 '
530 ' NAME IS FIRST 8 BYTES
540 '
550 NM$(DE)=LEFT$(E$,8)
560 '
570 ' EXTENSION IS BYTES 9-11
580 '
590 EX$(DE)=MID$(E$,9,3)
600 '
610 ' FILE TYPE IS BYTE 12
620 '
630 FT(DE)=ASC(MID$(E$,12,1))
640 '
650 ' ASCII FLAG IS BYTE 13
660 '
670 AF(DE)=ASC(MID$(E$,13,1))
680 '
690 ' FIRST GRANUAL IS BYTE 14
700 '
710 FG(DE)=ASC(MID$(E$,14,1))
720 '
730 ' BYTES USED IN LAST SECTOR
740 ' ARE IN BYTES 15-16
750 '
760 BU(DE)=ASC(MID$(E$,15,1))*256+ASC(MID$(E$,16,1))
770 '
780 ' IF FIRST BYTE IS 255, END
790 ' OF USED DIR ENTRIES
800 '
810 IF LEFT$(NM$(DE),1)=CHR$(255) THEN 1500
820 '
830 ' IF FIRST BYTE IS 0, FILE
840 ' WAS DELETED
850 '
860 IF LEFT$(NM$(DE),1)=CHR$(0) THEN 1480
870 '
880 ' SHOW DIRECTORY ENTRY
890 '
900 PRINT NM$(DE);TAB(9);EX$(DE);" ";FT$(FT(DE));" ";
910 IF AF(DE)=0 THEN PRINT"BIN"; ELSE PRINT "ASC";
920 '
930 ' CALCULATE FILE SIZE
940 ' SZ - TEMP SIZE
950 ' GN - TEMP GRANULE NUM
960 ' SG - SECTORS IN LAST GRAN
970 ' GC - GRANULE COUNT
980 '
990 SZ=0:GN=FG(DE):SG=0:GC=0
1000 '
1010 ' GET GRANULE VALUE
1020 ' GV - GRAN VALUE
1030 '
1040 GV=FA(GN):GM(GC)=GN:GC=GC+1
1050 '
1060 ' IF TOP TWO BITS SET (C0
1070 ' OR GREATER), IT IS THE
1080 ' LAST GRANULE OF THE FILE
1090 ' SG - SECTORS IN GRANULE
1100 '
1110 IF GV>=&HC0 THEN SG=(GV AND &H1F):GOTO 1280
1120 '
1130 ' IF NOT, MORE GRANS
1140 ' ADD GRANULE SIZE
1150 '
1160 SZ=SZ+2304
1170 '
1180 ' MOVE ON TO NEXT GRANULE
1190 '
1200 GN=GV
1210 GOTO 1040
1220 '
1230 ' DONE WITH GRANS
1240 ' CALCULATE SIZE
1250 '
1260 ' FOR EMPTY FILES
1270 '
1280 IF SG>0 THEN SG=SG-1
1290 '
1300 ' FILE SIZE IS SZ PLUS
1310 ' 256 BYTES PER SECTOR
1320 ' IN LAST GRAN PLUS
1330 ' NUM BYTES IN LAST SECT
1340 '
1350 SZ(DE)=SZ+(SG*256)+BU(DE)
1360 PRINT " ";SZ(DE)
1370 '
1380 ' SHOW GRANULE MAP
1390 '
1400 C=0:PRINT " ";
1410 FOR I=0 TO GC-1
1420 PRINT USING"##";GM(I);
1430 C=C+1:IF C=10 THEN PRINT:PRINT " ";:C=0 ELSE PRINT " ";
1440 NEXT:PRINT
1450 '
1460 ' INCREMENT DIR ENTRY
1470 '
1480 DE=DE+1
1490 NEXT:NEXT:NEXT
1500 END
1510 ' SUBETHASOFTWARE.COM

Since there is only one big file on this disk, fully filling it, it only has one 68-entry granule chain to print. I modified the code to PRINT#-2 these values to the virtual printer so I could then copy the numbers into the same spreadsheet:

 68 Files  Big File
FILE GRAN GRAN
0 32 32
1 33 33
2 34 34
3 35 35
4 30 36
5 31 37
6 36 38
7 37 39
8 28 40
9 29 41
10 38 42
11 39 43
12 26 44
13 27 45
14 40 46
15 41 47
16 24 48
17 25 49
18 42 50
19 43 51
20 22 52
21 23 53
22 44 54
23 45 55
24 20 56
25 21 57
26 46 58
27 47 59
28 18 60
29 19 61
30 48 62
31 49 63
32 16 64
33 17 65
34 50 66
35 51 67
36 14 30
37 15 31
38 52 28
39 53 29
40 12 26
41 13 27
42 54 24
43 55 25
44 10 22
45 11 23
46 56 20
47 57 21
48 8 18
49 9 19
50 58 16
51 59 17
52 6 14
53 7 15
54 60 12
55 61 13
56 4 10
57 5 11
58 62 8
59 63 9
60 2 6
61 3 7
62 64 4
63 65 5
64 0 2
65 1 3
66 66 0
67 67 1

Now it seems clearly obvious that RS-DOS does something different when making a new file, versus what it does when expanding an existing file into a new granule.

I wanted a way to visualize this so, of course, I wrote a program to help me create a full ASCII representation of the granules, then edited the rest by hand.

                           68    1 Big
Files File
Track 0 +------------+
| Granule 0 | 64 66
| Granule 1 | 65 67
Track 1 +------------+
| Granule 2 | 60 64
| Granule 3 | 61 65
Track 2 +------------+
| Granule 4 | 56 62
| Granule 5 | 57 63
Track 3 +------------+
| Granule 6 | 52 60
| Granule 7 | 53 61
Track 4 +------------+
| Granule 8 | 48 58
| Granule 9 | 49 59
Track 5 +------------+
| Granule 10 | 44 56
| Granule 11 | 45 57
Track 6 +------------+
| Granule 12 | 40 54
| Granule 13 | 41 55
Track 7 +------------+
| Granule 14 | 36 52
| Granule 15 | 37 53
Track 8 +------------+
| Granule 16 | 32 50
| Granule 17 | 33 51
Track 9 +------------+
| Granule 18 | 28 48
| Granule 19 | 29 49
Track 10 +------------+
| Granule 20 | 24 46
| Granule 21 | 25 47
Track 11 +------------+
| Granule 22 | 20 44
| Granule 23 | 21 45
Track 12 +------------+
| Granule 24 | 16 42
| Granule 25 | 17 43
Track 13 +------------+
| Granule 26 | 12 40
| Granule 27 | 13 41
Track 14 +------------+
| Granule 28 | 8 38
| Granule 29 | 9 39
Track 15 +------------+
| Granule 30 | 4 36
| Granule 31 | 5 37
Track 16 +------------+
| Granule 32 | 0 0 <- both start the same
| Granule 33 | 1 1
Track 17 +------------+
| FAT & |
| Directory |
Track 18 +------------+
| Granule 34 | 2 2
| Granule 35 | 3 3
Track 19 +------------+
| Granule 36 | 6 4 <- then big file continues
| Granule 37 | 7 5 writing to the end
Track 20 +------------+
| Granule 38 | 10 6
| Granule 39 | 11 7
Track 21 +------------+
| Granule 40 | 14 8
| Granule 41 | 15 9
Track 22 +------------+
| Granule 42 | 18 10
| Granule 43 | 19 11
Track 23 +------------+
| Granule 44 | 22 12
| Granule 45 | 23 13
Track 24 +------------+
| Granule 46 | 26 14
| Granule 47 | 27 15
Track 25 +------------+
| Granule 48 | 30 16
| Granule 49 | 31 17
Track 26 +------------+
| Granule 50 | 34 18
| Granule 51 | 35 19
Track 27 +------------+
| Granule 52 | 38 20
| Granule 53 | 39 21
Track 28 +------------+
| Granule 54 | 42 22
| Granule 55 | 43 23
Track 29 +------------+
| Granule 56 | 46 24
| Granule 57 | 47 25
Track 30 +------------+
| Granule 58 | 50 26
| Granule 59 | 51 27
Track 31 +------------+
| Granule 60 | 54 28
| Granule 61 | 55 29
Track 32 +------------+
| Granule 62 | 58 30
| Granule 63 | 59 31
Track 33 +------------+
| Granule 64 | 62 32
| Granule 65 | 63 33
Track 34 +------------+
| Granule 66 | 66 34
| Granule 67 | 67 35 <- then big file continues
+------------+ at Track 16

Interesting! For small files, it alternates tracks starting before Track 17 (FAT/Directory) then after, repeating. For a big file, it starts like that before Track 17, then after and continues to the end of Track 35, then goes before Track 17 and works back to the start of the disk.

Do I understand the sequence correctly?

To be continued…

DS-69 digitizer revisited

The Micro Works Digisector DS-69 / DS-68B digitizers were really cool tech in the 1980s. Looking back, I got to play with video digitizers, the Super Voice speech synthesizer that could “sing”, and even the E.A.R.S. “electronic audio recognition system” for voice commands. All of this on my Radio Shack Color Computer 3 in the late 1980s.

How many decades did it take for this tech to become mainstream in our phones or home assistants? We did it first ;-)

The DS-69 could capture 128×128 or 256×56 photos with 16 grey levels (4-bit greyscale). It also had a mode where it would capture 64 grey scales, though there was no viewer for this and I cannot find any attempts I made to use this mode.

I did, however, find some BASIC which I *think* I wrote that attempted to read a .PIX file and print it out to a printer using different ASCII characters to represent 16 different levels of grey. For example, a space would be bright white at level 0, and a “#” might be the darkest at level 15.

First, GREYTEST.BAS just tried to print blocks using these characters. I was testing.

5 DIM GR(15):FORA=0TO15:READGR(A):NEXT
10 PRINT#-2,"Grey Scale Printer Test:":PRINT#-2
15 FORA=0TO10:FORB=0TO15:PRINT#-2,STRING$(5,GR(B));:NEXT:PRINT#-2:NEXT
99 END
100 REM * Grey Scale Characters (0-15)
105 DATA 32,46,58,45,105,43,61,84,86,37,38,83,65,36,77,20

I asked the Google search engine, and its Gemini A.I. answered:

Dec.  ASCII
Value Character
----- ---------------------------
32 Space (invisible character)
46 . (period or full stop)
58 : (colon)
45 - (hyphen or minus sign)
105 i (lowercase i)
43 + (plus sign)
61 = (equals sign)
84 T (uppercase T)
86 V (uppercase V)
37 % (percent sign)
38 & (ampersand)
83 S (uppercase S)
65 A (uppercase A)
36 $ (dollar sign)
77 M (uppercase M)
20 NAK (Negative Acknowledge - a non-printable control character)

I must have been manually counting how many “dots” made up the characters and sorting them. I recall starting with the HPRINT font data in ROM (which is what my MiniBanners program used) to count the set dots in each letter, but the printer fonts would be different so I expect this table came from trial and error.

The 20 NAK (non printable) is an odd one, so I wonder if my printer DID print something for that – like a solid block graphic.

Proving memory is not always faulty, I also found TEST.BAS which appeared to open a .PIX file and print it out using this code:

0 POKE150,44:PRINT#-2,CHR$(27)CHR$(33)CHR$(27)CHR$(77)CHR$(27)CHR$(64)CHR$(15)
1 PRINT#-2
5 DIM GR(15):FORA=0TO15:READGR(A):NEXT
10 OPEN"D",#1,"SMILE.PIX",1:FIELD#1,1ASA$
11 PRINTLOF(1)
15 FORA=1TO64:PRINTA:FORB=0TO127:GET#1,A+B*64:GR=ASC(A$)
20 PRINT#-2,CHR$(GR(GR AND15));
25 NEXT:PRINT#-2:NEXT:CLOSE
99 END
100 REM * Grey Scale Characters (0-15)
105 DATA 32,46,58,47,62,63,61,84,86,37,38,90,65,69,77,35

I see line 10 opens the file with DIRECT mode with a field size of 1 assigned to string variable A$. This means doing a GET #1,X (where X is a byte offset in the file) would get that byte into A$ so I could get the ASCii value of it (0-15) and use that to know which character to print.

I have no idea if this worked… So let’s give it a try.

I see the program print “8192”, which is the Length Of File. A 128×128 image of bytes would be 16384 in size, so I am guessing each byte has two pixels in it, each 4-bits.

I see I am ANDing off the upper bits in line 20. It looks like I am throwing away every other pixel since no attempt I made to read those other 4-bits. This is likely because this was printing on an 80 column printer, which would not print 128 characters on a line. Instead, 64 would fit.

And, wow! It actually works! I had to reduce the font size down for it to display in the WordPress blog, but here is the output. Step back from the monitor if you can’t see it.

################################################################################################################################
################################################################################################################################
///////////::/:/:::.:::::..:.:: ::/.:://///>>=%V%%V%TT===>>>//?>::.. :. . .. . :.. . ::: . :::.:::::::://:::/:::://////////////
///////:////:://::::.::::...:::.::////>/>?=%EAEMAMEEEMEAAME&%VT=?>//::::. .: . .::.. :::.. .. .::::::::.//:://///////////>/////
//////://///:://::::.:::::.:::://>/??TV%%&EMMMMMEEMMMMMAEMMEEAA&ZVT=?>::.. .: . ... :::...:.:.::::.:::..:::////://////////////
///////:///:::./.::...:::::////??V%AEEMM##MM###MMMMMMMMMMEAEMMMEEZZZ&V=>//::: :... ::: .:::.::..:::::/::////////////////////
//////:://::::/::::: :::///?T%AAM#MMMM#M########M#MMM#MMAEMAEMMAAA&ZZ&V=/:: . :.. :: . ..:.:: :::::/:::////://////////////
/////:::://:::./ .:. :.::?/?T%ZEMEMMMMM###########M#MM##M#MMMAAAEEMEEMMAZZZT>/:. :: . : . ... :: :::::.:::::://://////////////
/////:::::::::.. ::. ./?>?T&AAEMMMMMMM#MM########MMMMMMM###MMMAAAAAAEMMMMMEA&=//::: ... . .:.. .:::...::::/:://////////////
/////::::.::::: :: :?=T%MEEEEMMMMMMMMMM#####M#MMMEAEAMM##MMMMMEEEAAEMMEM#MAMT>/:::. .: . .. . ::... :::::/:::////:///////
/://::::...::. . .::>=V%AEMEAEEMMEMMEMEMMMMM#M#MEEAZZ&%&ZEEMMAEMMMMMMMMMMMMMMEAZT>//:. . . . .. :::.. :::.::::://:::///////
:::::::::. .:. :?TT&AEEEZAEEEEZEZZZEEMEEEMMMM#MAAZ&&V&V&ZAMMMAMM###MMMM#M#MEMAAT?//: :. . . . :::. ::...:::/:/:::////://
:::. ::: :: .:/?T%&EM#AAEMMAEZA&ZAZZEAAZAMMMMMMAAAZAVVVV%&AM#MMEMM##MMM####MMMAZ%>//:: . . . . :::.. :. ..:::::/:::///:://
:::: ::: ::.:>?VZAEMMAZEMMEAAAZZAZAZEAAEAAZEAMEMAA&&%VVVV&AMM#MMMMM#######M###M&V?//::: . . . :: . :... :::::/:::///:://
.::. .:. ::/?%AAAMMEAEMMEMAAZAAAZAAEAAZZ%%%ZAEEAA&Z&%V%%&AAM#MMMMM#######MM#M#ZTV>//:: . . . ..: :. . :::::/:::////://
.:. .:: . .:/>%EEAEMEEE#MMMMAAAEAZZZEA&ZZZVVVVZZAZAZAZZ%V&&AMEMMM##MM#M######M#MM&T?//:::. . . ... :... :::::::::///:://
.::.. .: . .:=VZMZEMEMMMM#MEEAAMAZZ&AA%&&Z%TVTTV&&&&ZEEZZZV&ZA#MMMM#############MEZ=T>/::/. . . :: . :..../:::./:::/.::::/
.::. .. . :T%MEAMEEMMMMMEMAAEA&%%ZZ%VV&&TTTTTTTV&&ZAEAZAE&&ZAEMM########M#M#M#MMMM&?/::/. . . .: :. . .:: ..:::///:::/
.. .: . /T&EZEEMMMMMMMEAZEZ%VV&&&%VVZVTTTTTTTTVT%&EAMEEMAZAME#M#M######MMMM##MMMZT?/:/. .. . .: :. :.:..::::///::/:
.. .. . :T&&&EMMMMM##MMEMA&%VVTVTVT%%VTTTTTTTT=TTTVZZAMMME&VTTTVZEM#ZAM########MM%?///:... . ... .... .:: ..::::..:::/
:. . .:T&&AEAMM#####MME%V%VVTTTTTT%VTTTTTTTTTTTTV%TVV&VV=. .>=T%ZMZAEM##MMMMMZ&VV?///:: . .: :: :.: ::::/:/:::/
:.. . . :?%&AM#MM#####EMZVVTVTTTTTTVV&TTTTTTTTT%&VTTTTTTTTTTTTTTTTTT%&EEE#####MMA&%T?///:::.. :. :. . :::: .:::://:::/
:. . ../=VZMM#######MEAAEZ%VTTTVVVV%%VVTT=T=TTTTTTTT=>TTTTTTTTTTVTT&TT&E######MEV=/////::.:. :: . :. . .::.:.:::://:::/
:. .. ...::/VZMM#######E%TTTTVVTTTTVTTTTTEVT===ZTTTVTV%? ?VZZ%V%&VVVT=VTTTTZM#######MZVTT=>:::: :. :: . ..: ..:::::::://
.. . .. . ..: =ZMM##VZEEEZ&TVVTTTTTTTVTTTVV&M#TZE#%TTTV%ZV&EEEATTTTTTTTTVTTTTT&MEAAEE#M#MZTT?>?>//:.::. ::.. ..: :::/.::::/
:. . :..?&E###MMM#MZ%VTTTTT/ .=T&V&V%%EZT=T%ATTTTTTTTTT=TTTTTTTTTTVTTT=TVZMEAZZZEM##M&=>/:/::.:: . :: . .: .::::::/:://
:. . .....>%M#M##M##MZ%TTVVZZAEA##ZVTTV&ET=>>=%ETTTTTTTTTTTTT===TTT=%TTTTTTZEE&&&%A&###M%=>>///::::. :: . .: . ::::/::://
: .. :::=VZ#M#####MZTTVV%VTTTTTVVTTV%&ZT===?=TA%T=TTTT=====??=TTTVVTTTTTV&AAAEZZVVAMM#E&T>///:::. ::: ..: ..::://::://
:. : .://VA#M#####E%TTTTTTTTTTT=TTTTTATT=>=?=??%Z%TTTT==??>/??TV%TTTTTTTTAE#MAAE&VE###EA&?///:::. :: . ..: ..:::::/:://
: . . . ....>VE#M####METTTTTTTTT==TTTTTTZZTT==>?=?T==TZZZV=TV%%%EZ%TTT=TTTTTTV&%%&ZAZZ####EAZT>///::.. ::. . ..: . :::.:::://
.: : . .::V&M#######ATTTTTTTTT===?==VAE%T==>??=??==TTTTTTT======TTT=T=TTTTTTTVVTT&EM###M#EAV>///:::. ::: .: .:::://:/://
:. .. . .::=&E########EETTTTTTTT===T%&T%%TVVTTTVVZE%==TTTTT=T=?====?====TTTTTT=?TTTTAMMMMEMAZVT??//://:::: . .. ..:::..::://
.: . .: .::/?%MM##MMM#MMMZ%Z%ZZ&ZEAZ%TVTTTVV%MA%VV&TV&VVTTTTTVTTTT=TTTTTTTTTTTT==???TVEMMM#MMAVT=///////.::: . ..: ..:::///:://
.: ..:.:>TVZMM###EEAMMM#A%TTTTTTTTTTTTTVTV%V%T%VVTTTT====?==TVTTTTTTTTTTTTTT=VT==TT%AM####EAZ?>/////////:::.. .:...::://::://
.: . .:::/?T%E#####E&AEMEMMZTTVTTVTTTTTTTVVTTVTTTTTTT=????=?>=TTTTTTTTTTTT=TTTTZ###########M&&V?>/////://:::::. ..: ..:::.:::://
.:. ..::/=%AAM######A&A#MMMEVTVTVTTTTTTVTTTTVTTTT===?=?===?====TVVTT==??====?=TZ#########MMMZ%?>/////:::..::: ....:.. :::./::://
.:...::/TV&V&M#######MMMMMME%TVVVVTTTTVTTTTTVVV%%VVVV%%%%%%VZ&ZMM&TT==?>===T?=VE##########ME%=//////::::: ::: ::... ..:::///:::/
.::/:/>=T=>TZM##M#M#######EMZVVT%VTTTTTV&&ZZAVTTT?//=>/?//?/??TZV%T==???===TTT%#############ET>/////::::..::: : .::.. :::.:/:://
:::////////TTAMMEMM######MMMM%VVVTTTTTTT&##AVT==>>>?===T==V=&TTT=T===>?====TTTEM#########EMM#V?/////:::::.::: : :.:..:::://::://
:://////////=TZMM#M###########%VVTTT=T=TTTTVVTTTVVVTTTTTTTTTTTTT==?=?>?T==TTTAM#########MMMZA&=>>///::::..::....::: .::::///:///
//////////>>TVE###############E%VVTTTT==T=TTTTTTTTTTTTTTTTTTT===>>??===?=TTT&M###########MMEAAT>>///:::::/:::...:::...:::/:::///
/////////>=TZM#################AVVTTTTTTT==TTTTTTTT=T??===?T===>>>>?=TTT=TV&Z###########MMMEAAV?>///:::::::::::.:::../:::://:://
////////?T%EM###M###############MVVTTTTTTTTTTTT=TTTT==???=>=>?>>>>?==TTTT%&&E######M###M##MEMA&?>////::/./::::..::: ./:::///////
///>>>>=V&AM##MMM################MZ%TTVTTTTTTT==?==???>///?>/??>??===TVV%Z&VA########MEMMMM#ME&T>>///::://:::::/::: .::::///:://
/>>>>?TV%EM#MME&M##################MZVVVTTT==T==>=?>/>>/>>?>/>=T=TTTTVV&&%%%E###########M#M###M&=>///::/:::::::/:::../:::///////
/>/>?TVA%AMMME%&M###################EZ%VTTTTTT==?=?>??>?????=TTTVVV&&%&%%VV%M##################AV>////:://:::::/:::::/:/:///:///
///=TV%%VMMEM&TZM###################EZA%V%VVVTTTTT=TTTT==TTTTVVVVV&&&&VVTT&AM##################M%?////:///::::/:::::./::////////
>??TVV%T%MEAA%VZA###################EA%%&Z%%VVV%TVTVTTTTVV&&VVV%%&Z&%%VVT%AM###############M###M%?/////////::///::::./:::///////
==TVT%TV%MMAEVV&A###################MA&V&%%VVVVVTVVVTV%VVV%&V%%%&&%%TVVV%AAM##############MM##MEV?////////:::///:::/./:::///////
TVV%V&VAMMMMMVVZE########MMMM########MA%%%VVVV%%VVVVTV%V%V&&&Z&%V%VTTVVVZM#M#############MMM#ME&=>/////////::///:::////::///////
TV%&ZZEAM####MM############MMM#######MMZ&%VVVVVV%VVV%%&VV&&&%%V%VTTTT%ZAE#####M##MMMM####MMMMA&T>>/>///////::///:::::///////////
T%V&&MAMM####################M########MEZ&%VVVTTVVVVT&VVV%%%%VVTTTVV&AEM###M###############MMAVT>>/>/////////////>>?????>///////
T&%&&EAM################################EA&VVVVTTVVTTTTVTVTVVTTTVV%AAMM##################MMMEA%TT?===TTT===TVTTTTTTVTTTTTTTT=?>>
TVVT%&EEM##############################MMEZ&%VVTTVVTTTTVVTTTTTVVV%ZAM#M#M################MMMMM##MMEZZ%%V%%VVVVTTTTVTTTTVVTVTTTVT
TVVV%V&AM#M#M###########################MEAZ&%%TVTVTTTTTVVTTV%VT%EMEEMMMM######################MMMAZZAAZAZ&&V%VVTTVTTTTTVTTTTTVT
T&VT%V%&ZM####M#M#######################MMZZ&%%%%VVVTVVVVVVVTTVV&EEEAMEME####M####################M#MMMMEEAEZ&&ZTTVTTVTTVTVTTTVT
TVVT%VTVVZM#MMMMMM#MM################MMMMMMA%&&ZVTVVVVTVVTTTTTVZAEAAZAAEEMM######################M#####MEAAAMEZZTTVTTTTTVTTTTTVT
TV%T%TTVV%EMMM###MMMMMM#MMMEMEEEMEMMM###MMEEZV&VVTVTTTTTTTTTTTV&V&ZAZEMMMM########################MMEMMMMEMMMMZ%TVVTTTTTTTTVTTVV
TVVT%TTVT%ZM###M#M##MMMAAAAZAAEZEEEMEMM#MEAMZZ%%TVTTTTTTTTTTTVVV&ZZAZMM#######################MM####MMMMEMEMMZ%TTTVT==T=TTTVTTVT
TVVV%VVVV%&AMEEEE#EMM#MMM#MMMMEEAAMEEMMMMMAAEZ&&TVVTTTTTVTTTTVV&%&ZEMMM#############MMMM#######MMMMMMMMEAAAZE%TVTTTTTTT==TTTTTTT
TVTV%TVVV%EAEEAAAZEAZEEEEEEAAEAEZAAEAMMEAEAAAZ%VTTTTTTTTTTTTTVVT%&EE################MMM########MMMEEMMEE%%VTVVTTTTVT==T==TTTTTTT
V%V%%TV%VZMMMAMMEEZZAZAMAA&&Z&AEEMEAEEEAZAZ&&%TTTTTTTTTTVTTT%%%V&AE#MM########MMM#M##MMM########MMMAAA%VVTTTVTTTTTTTT====TTVTTTT
################################################################################################################################
################################################################################################################################

And here is a screenshot of it, if that did not work:

DS-69 .PIX file printed in ASCII.

Well that’s neat. I wonder what I did with this.

Until next time…

Tackling the Logiker 2025 Vintage Computing Christmas Challenge – part 1

See Also: Prologue and part 1.

Rules to the Challenge

❄️ Vintage Computing Christmas Challenge 2025 ❄️ – Logiker

The Pattern

Observations

  1. Since this is a symmetrical pattern, if we can figure out how to draw one quadrant, we can draw the others.
  2. The pattern is 19 characters wide, which contains a center column of asterisks, and a left and right column that are spaces except for the center row of asterisks.
  3. “As if they had planned it,” this means the pattern in each quadrants is 8 characters, matching the number of bits in a byte.

I typed it up to figure out what the bit pattern would be. (Actually, I typed up a bit of it, then pasted that into Copilot and had it tell me the bit pattern.)

.        *
.------*- = 2
.-*-*---* = 81
.--**---- = 48
.-***--*- = 114
.----*--* = 9
.-----*-- = 4
.*-*---*- = 162
.-*-*---* = 81
*******************

That’s a mess, but in the left the “.” would represent the blank space down the left side up to the row of 19 asterisks. After that is the 8-bit pattern with “-” representing a space in the pattern (0 bit) and the “*” representing the asterisk (1 bit).

This let me quickly cobble together a proof-of-concept:

1 READ V
2 A$=STRING$(19,32):MID$(A$,10,1)="*"
3 FOR B=0 TO 7
4 IF V AND 2^B THEN MID$(A$,9-B,1)="*":MID$(A$,B+11,1)="*"
5 NEXT
6 PRINT A$:A$(L)=A$
7 L=L+1:IF L<8 THEN 1
8 PRINT STRING$(18,42)
9 FOR B=7 TO 0 STEP -1:PRINT A$(B):NEXT
10 DATA 2,81,48,114,9,4,162,81
  • Line 10 are the 8 rows of byte data for a quadrant of the snowflake.
  • Line 1 reads the first value from the DATA statement.
  • Line 2 builds a string of 19 spaces, then sets the character at position 10 (in the center) to an asterisk. Every row has this character set.
  • Line 3 begins a loop representing each bit in the byte (0-7).
  • Line 4 checks the read DATA value and ANDs it with the bit value (2 to the power of the the FOR/NEXT loop value). If it is set, the appropriate position in the left side of the string is set to an asterisk, and then the same is done for the right side. To mirror, the left side is center-minus-bit, and the right side is center-plus-bit.
  • Line 5 is the NEXT to continue doing the rest of the bits.
  • Line 6 prints the completed string, then stores that string in an A$() array. L has not been used yet so it starts at 0.
  • Line 7 increments L, and as long as it is still ess than 8 (0-7 for the first eight lines of the pattern) it goes back to line 1 to continue with the next DATA statement.
  • Line 8 once 8 lines have been done, the center row of 19 asterisks is printed.
  • Line 9 is a loop to print out the A$() lines we saved, backwards. As they were built in line 6, they went from 0 to 7. Now we print them backwards 7 to 0.

…and there we have a simple way to make this pattern, slowly:

Logiker 2025 pattern on a CoCo.

On a CoCo 3, adding a WIDTH 40 or WIDTH 80 before it would show the full pattern:

Logiker 2025 pattern on a CoCo 3.

My example program can be made much smaller by packing lines together and removing unnecessary spaces. One minor optimization I already did was doing the bits from 0 to 7 which removed the need to use “STEP -1” if counting backwards. Beyond that, this is the raw proof-of-concept idea of using bytes.

Other options folks have used in past challenges included rune-length type encoding (DATA showing how many spaces, then how many asterisks, to make the pattern) so that probably is worth investigating to see if it helps here.

Then, of course, someone will probably figure out a math pattern to make this snowflake.

What thoughts do you have?

CoCo Disk BASIC disk structure – part 3

See also: part 1, part 2 and part 3.

A correction, and discovering the order RS-DOS writes things…

A correction from part 2… This example program had “BIN” and “ASC” mixed up. 0 should represent BINary files, and 255 for ASCii files. I fixed it in line 920. (I will try to edit/fix the original post when I get a moment.)

10 ' FILEINFO.BAS
20 '
30 ' 0.0 2023-01-25 ALLENH
40 ' 0.1 2023-01-26 ADD DR
50 ' 0.2 2023-01-27 MORE COMMENTS
55 ' 0.3 2025-11-18 BIN/ASC FIX
60 '
70 ' E$(0-1) - SECTOR HALVES
80 ' FT$ - FILE TYPE STRINGS
90 '
100 CLEAR 1500:DIM E$(1),FT$(3)
110 FT$(0)="BPRG":FT$(1)="BDAT":FT$(2)="M/L ":FT$(3)="TEXT "
120 '
130 ' DIR HOLDS UP TO 72 ENTRIES
140 '
150 ' NM$ - NAME
160 ' EX$ - EXTENSION
170 ' FT - FILE TYPE (0-3)
180 ' AF - ASCII FLAG (0/255)
190 ' FG - FIRST GRANULE #
200 ' BU - BYTES USED IN LAST SECTOR
210 ' SZ - FILE SIZE
220 '
230 DIM NM$(71),EX$(71),FT(71),AF(71),FG(71),BU(71),SZ(71)
240 '
250 INPUT "DRIVE";DR
260 '
270 ' FILE ALLOCATION TABLE
280 ' 68 GRANULE ENTRIES
290 '
300 DIM FA(67)
310 DSKI$ DR,17,2,G$,Z$:Z$=""
320 FOR G=0 TO 67
330 FA(G)=ASC(MID$(G$,G+1,1))
340 NEXT
350 '
360 ' READ DIRECTORY
370 '
380 DE=0
390 FOR S=3 TO 11
400 DSKI$ DR,17,S,E$(0),E$(1)
410 '
420 ' PART OF SECTOR
430 '
440 FOR P=0 TO 1
450 '
460 ' ENTRY WITHIN SECTOR PART
470 '
480 FOR E=0 TO 3
490 '
500 ' DIR ENTRY IS 32 BYTES
510 '
520 E$=MID$(E$(P),E*32+1,32)
530 '
540 ' NAME IS FIRST 8 BYTES
550 '
560 NM$(DE)=LEFT$(E$,8)
570 '
580 ' EXTENSION IS BYTES 9-11
590 '
600 EX$(DE)=MID$(E$,9,3)
610 '
620 ' FILE TYPE IS BYTE 12
630 '
640 FT(DE)=ASC(MID$(E$,12,1))
650 '
660 ' ASCII FLAG IS BYTE 13
670 '
680 AF(DE)=ASC(MID$(E$,13,1))
690 '
700 ' FIRST GRANUAL IS BYTE 14
710 '
720 FG(DE)=ASC(MID$(E$,14,1))
730 '
740 ' BYTES USED IN LAST SECTOR
750 ' ARE IN BYTES 15-16
760 '
770 BU(DE)=ASC(MID$(E$,15,1))*256+ASC(MID$(E$,16,1))
780 '
790 ' IF FIRST BYTE IS 255, END
800 ' OF USED DIR ENTRIES
810 '
820 IF LEFT$(NM$(DE),1)=CHR$(255) THEN 1390
830 '
840 ' IF FIRST BYTE IS 0, FILE
850 ' WAS DELETED
860 '
870 IF LEFT$(NM$(DE),1)=CHR$(0) THEN 1370
880 '
890 ' SHOW DIRECTORY ENTRY
900 '
910 PRINT NM$(DE);TAB(9);EX$(DE);" ";FT$(FT(DE));" ";
920 IF AF(DE)=0 THEN PRINT"BIN"; ELSE PRINT "ASC";
930 '
940 ' CALCULATE FILE SIZE
950 ' SZ - TEMP SIZE
960 ' GN - TEMP GRANULE NUM
970 ' SG - SECTORS IN LAST GRAN
980 '
990 SZ=0:GN=FG(DE):SG=0
1000 '
1010 ' GET GRANULE VALUE
1020 ' GV - GRAN VALUE
1030 '
1040 GV=FA(GN)
1050 '
1060 ' IF TOP TWO BITS SET (C0
1070 ' OR GREATER), IT IS THE
1080 ' LAST GRANULE OF THE FILE
1090 ' SG - SECTORS IN GRANULE
1100 '
1110 IF GV>=&HC0 THEN SG=(GV AND &H1F):GOTO 1280
1120 '
1130 ' ELSE, MORE GRANS
1140 ' ADD GRANULE SIZE
1150 '
1160 SZ=SZ+2304
1170 '
1180 ' MOVE ON TO NEXT GRANULE
1190 '
1200 GN=GV
1210 GOTO 1040
1220 '
1230 ' DONE WITH GRANS
1240 ' CALCULATE SIZE
1250 '
1260 ' FOR EMPTY FILES
1270 '
1280 IF SG>0 THEN SG=SG-1
1290 '
1300 ' FILE SIZE IS SZ PLUS
1310 ' 256 BYTES PER SECTOR
1320 ' IN LAST GRAN PLUS
1330 ' NUM BYTES IN LAST SECT
1340 '
1350 SZ(DE)=SZ+(SG*256)+BU(DE)
1360 PRINT " ";SZ(DE)
1370 DE=DE+1
1380 NEXT:NEXT:NEXT
1390 END
1400 ' SUBETHASOFTWARE.COM

To test this routine, I created a program that let me type a file size (in bytes) and then it would make a .TXT file with that size as the filename (i.e, for 3000 bytes, it makes “3000.TXT”) and then I could run it through this program and see if everything matched.

It opens a file with the size as the filename, then writes out “*” characters to fill the file. This will be painfully slow for large files. If you want to make it much faster, share your work in a comment.


10 ' MAKEFILE.BAS
20 '
30 ' 0.0 2025-11-18 ALLENH
40 '
50 INPUT "FILE SIZE";SZ
60 F$=MID$(STR$(SZ),2)+".TXT"
70 OPEN "O",#1,F$
80 FOR A=1 TO SZ:PRINT #1,"*";:NEXT
90 CLOSE #1
100 DIR
110 GOTO 50
120 ' SUBETHASOFTWARE.COM

I was able to use this program in the Xroar emulator to create files of known sizes so I could verify the FILEINFO.BAS program was doing the proper thing.

It seems to be, so let’s move on…

A funny thing happened on the way to the disk…

I have been digging in to disk formats (OS-9 and RS-DOS) lately, and learning more things I wish I knew “back in the day.” For instance, I was curious how RS-DOS allocates granules (see part 1) when adding files to the disk. I wrote a test program that would write out 2304-byte blocks of data (the size of a granule) full of the number of the block. i.e., for the first write, I’d write 2304 0’s, then 2304 1’s and so on. My simple program looks like this:

10 'GRANULES.BAS
20 OPEN "O",#1,"GRANULES.TXT"
30 FOR G=0 TO 67
40 PRINT G;
50 T$=STRING$(128,G)
60 FOR T=1 TO 18
65 PRINT ".";
70 PRINT #1,T$;
80 NEXT
90 PRINT
100 NEXT
110 CLOSE #1

I ran this on a freshly formatted disk and let it fill the whole thing up. The very last write errors with a ?DF ERROR (disk full) so it never makes it to the close. I guess you can’t write that last byte without an error?

Now I should be able to look a the bytes on the disk and see where the 0’s went, the 15’s went, and so on, and see the order RS-DOS allocated those granules.

I made a simple test program for this:

0 'GRANDUMP.BAS
10 CLEAR 512
20 FOR G=0 TO 67
30 T=INT((G)/2):IF T>16 THEN T=T+1
40 IF INT(G/2)*2=G THEN S1=10:S2=18 ELSE S1=1:S2=9
50 'PRINT "GRANULE";G;TAB(13);"T";T;TAB(20);"S";S1;"-";S2
54 DSKI$0,T,S1,A$,B$
55 PRINT "GRANULE";G;ASC(A$)
60 NEXT G

Ignore the commented out stuff. Initially I was just getting it to convert a granule to Track/Sectors with code to skip Track 17 (FAT/Directory). And, to be honest, I had an AI write this and I just modified it ;-)

I then modified it to PRINT#-2 to the printer, and ran it in Xroar with the printer redirected to a text file. That gave me the following output:

GRANULE 0  67
GRANULE 1 66
GRANULE 2 65
GRANULE 3 64
GRANULE 4 63
GRANULE 5 62
GRANULE 6 61
GRANULE 7 60
GRANULE 8 59
GRANULE 9 58
GRANULE 10 57
GRANULE 11 56
GRANULE 12 55
GRANULE 13 54
GRANULE 14 53
GRANULE 15 52
GRANULE 16 51
GRANULE 17 50
GRANULE 18 49
GRANULE 19 48
GRANULE 20 47
GRANULE 21 46
GRANULE 22 45
GRANULE 23 44
GRANULE 24 43
GRANULE 25 42
GRANULE 26 41
GRANULE 27 40
GRANULE 28 39
GRANULE 29 38
GRANULE 30 37
GRANULE 31 36
GRANULE 32 1
GRANULE 33 0
GRANULE 34 3
GRANULE 35 2
GRANULE 36 5
GRANULE 37 4
GRANULE 38 7
GRANULE 39 6
GRANULE 40 9
GRANULE 41 8
GRANULE 42 11
GRANULE 43 10
GRANULE 44 13
GRANULE 45 12
GRANULE 46 15
GRANULE 47 14
GRANULE 48 17
GRANULE 49 16
GRANULE 50 19
GRANULE 51 18
GRANULE 52 21
GRANULE 53 20
GRANULE 54 23
GRANULE 55 22
GRANULE 56 25
GRANULE 57 24
GRANULE 58 27
GRANULE 59 26
GRANULE 60 29
GRANULE 61 28
GRANULE 62 31
GRANULE 63 30
GRANULE 64 33
GRANULE 65 32
GRANULE 66 35
GRANULE 67 34

Now I can see the order that RS-DOS allocates data on an empty disk.

The number in the third column represents the value of the bytes written to that 2304 granule. When I see “GRANULE 67” contains “34” as data, I know it was the 35th (numbers 0-34) granule written out.

Granules 0-33 are on tracks 0-16, then track 17 is skipped, then the remaining granules 34-67 are on tracks 18-34.

You can see that RS-DOS initially writes the data close to track 17, reducing the time it takes to seek from the directory to the file data. This makes sense, though as a teen, I guess I had some early signs of O.C.D. because I thought the directory should be at the start of the disk, and not in the middle ;-)

I brought this data into a spreadsheet, then sorted it by the “data” value (column 3). This let me see the order that granules are allocated (written to). I will add some comments:

GRANULE	33	0 <- first went to gran 33
GRANULE 32 1 <- second went to gran 32

...then it starts writing after Track 17...

GRANULE 35 2 <- third went to gran 35
GRANULE 34 3 <- fourth went to gran 34
GRANULE 37 4
GRANULE 36 5
GRANULE 39 6
GRANULE 38 7
GRANULE 41 8
GRANULE 40 9
GRANULE 43 10
GRANULE 42 11
GRANULE 45 12
GRANULE 44 13
GRANULE 47 14
GRANULE 46 15
GRANULE 49 16
GRANULE 48 17
GRANULE 51 18
GRANULE 50 19
GRANULE 53 20
GRANULE 52 21
GRANULE 55 22
GRANULE 54 23
GRANULE 57 24
GRANULE 56 25
GRANULE 59 26
GRANULE 58 27
GRANULE 61 28
GRANULE 60 29
GRANULE 63 30
GRANULE 62 31
GRANULE 65 32
GRANULE 64 33
GRANULE 67 34
GRANULE 66 35

...now that it has written to the final Track 35 (gran 66-67)...

GRANULE 31 36 <- before Track 17 and the original writes.
GRANULE 30 37
GRANULE 29 38
GRANULE 28 39
GRANULE 27 40
GRANULE 26 41
GRANULE 25 42
GRANULE 24 43
GRANULE 23 44
GRANULE 22 45
GRANULE 21 46
GRANULE 20 47
GRANULE 19 48
GRANULE 18 49
GRANULE 17 50
GRANULE 16 51
GRANULE 15 52
GRANULE 14 53
GRANULE 13 54
GRANULE 12 55
GRANULE 11 56
GRANULE 10 57
GRANULE 9 58
GRANULE 8 59
GRANULE 7 60
GRANULE 6 61
GRANULE 5 62
GRANULE 4 63
GRANULE 3 64
GRANULE 2 65
GRANULE 1 66
GRANULE 0 67 <- last write at the very first gran

And down the rabbit hole I go. Again. I have tasked an A.I. with creating some simple scripts to manipulate RS-DOS disk images (just for fun; the toolshed “decb” command already exists and works great and does more). While I understood the basic structure for an RS-DOS disk, I did not understand “how” RS-DOS actually allocated those granules. Now I have some insight. Perhaps I can make my tools replicate writing in the same way that RS-DOS itself does.

Look for a part 4. I have some more experiments to share.

To be continued…

Do you want to build a snow flake? Logiker 2025!

The 2025 edition of the Logiker programming challenge has been announced via a YouTube video:

This year’s pattern is a snowflake, and I am very curious to see the approaches people come up with in BASIC to do this. I have some ideas, but none of them seem small.

This 19×19 image won’t fit on a CoCo’s 32×16 screen, but the challenge allows it to scroll off as long as it was printed to match. Using the 40/80 column screen on the CoCo 3 would work well.

Here is the info page for the challenge:

https://logiker.com/Vintage-Computing-Christmas-Challenge-2025

I somehow completely missed out on last year’s challenge, which was a present box, so maybe I’ll find some time to experiment with this one. I’ve never “entered” the challenge, but have blogged attempts here.

Are you in? Let’s get coding!

Coding standards and talking myself into snake_case…

Obviously, things done “today” can be quite different than how things were done 30 years ago, especially in regards to computers. Is it strange that out of all the places I’ve worked where I wrote code that only one of them had an actual “coding standard” we had to follow? (And that was at a giant mega-corporation.) All the others seemed to have folks who just carried on how things were, for the most part, or were given the freedom to code as they wanted as long as they followed some specific system (such as “Clean Code” at a startup I worked at).

My day job has been undertaking an official coding standard. I started by documenting how things were done in the millions of lines of code we maintain. Over my years here, I have introduced some things from the mega-corporation’s standards into our code base, such as prefixing static variables with “s_” or globals with “g_” or similar.

But why reinvent the wheel? I thought it would make a lot more sense to find an existing standard that applied to embedded C programming and just adopt it. That led me to this:

https://barrgroup.com/embedded-systems/books/embedded-c-coding-standard1

This document clearly states the “why” for any requirement it has, and I have learned a few things. Unlike most standards that are mostly cosmetic (how to name functions or variables, how wide is a tab, etc.), this one focuses on bug detection, bug reduction and making code easier to review.

Which brings me to snake case…

https://en.wikipedia.org/wiki/Snake_case

Most places I have worked use camelCase, where all words are ran together (no spaces) with the first letter in lowercase, then all subsequent words starting with uppercase. itWorksButCanBeHardToRead.

The mega-corp standard I worked under would refer to “camel case starting with an uppercase letter,” which I have since learned is known as PascalCase. ItCanHaveTheSameProblem when the eyes have to un-smush all the letters.

snake_case is using lowercase words separated by underlines. A variant, SCREAMING_SNAKE_CASE uses all uppercase. (I never knew the name of that one, but most places I worked use that for #defines and macros and such.)

…and I expect someone will comment with even more naming conventions than I have encountered.

Do. Or do not. There is no try.

Beyond “how stuff looks,” some suggestions actually matter. Something I only started doing in recent times is when you put the constant on the left side of a comparison like this:

if (42 == answer)
{
    // Ultimate!
}

I saw this for the first time about a decade ago. All the code from a non-USA office we interacted with had conditions written “backwards” like that. I took an instant dislike to it since it did not read naturally. I mean who says “if 225 pounds or more is your weight, you can’t ride this scooter”?

However, all it takes is a good argument and I can flip on a dime. This flip was caused by finding multiple bugs in code where the programmer accidentally forgot one of the equals:

if (answer = 42)
{
// Ultimate?
}

If you use a modern compiler and have compiler warnings enabled, it should catch that unintended variable assignment. But, if you are using a “C-Like” embedded compiler that is missing a lot of modern features, it probably won’t. Or, if even you are running GCC with defaults:

https://onlinegdb.com/pv5Yz24t2

And thus, I broke my habit of making code that “reads more naturally” and started writing comparisons backwards since the compiler will fail if you do “42 = answer”.

This week, I learned they refer to this as Yoda conditions. Of course they do.

https://en.wikipedia.org/wiki/Yoda_conditions

Snakes. Why did it have to be snakes?

This week I read one sentence that may make me get away from camelCase and PascalCase and go retro with my naming convention. It simply had to do with readability.

is_adc_in_error is very easy to read. I’d say much easer than IsAdcInError which looks like some gibberish and requires you to focus on it. If anyone else is going to review the code, being able to “parse” it easier is a benefit.

I am almost convinced, even if I personally dislike it.

Give me some reasons to stick with camelCase or PascalCase, and let’s see if any of them are more than “’cause it looks purtier.” (For what its worth, I’ve seen camelCase for global functions, and CamelCase for static functions.)

Awaiting your responses…

Open Micro Works Digisector DS-69 digitizer .PIX files in GIMP

Step 1: Rename the .PIX file so it has the extension .data. This is needed for GIMP to recognize it as a “raw” data file.

Step 2: Open this image in GIMP by expanding “Select File Type” and choosing Raw image data. That should allow the .data file to show up in the browser to open it.

Step 3: The file will open and you must adjust settings to tell GIMP more about the image. Under Pixel format, select Grayscale 4-bit. For the Width and Height, set them to 256 (if it is a 32K file) or 128 (if it is 8K). Now you should be able to Open the image.

Step 4: With the image open, you will need to Invert it to get the colors correct (Colors -> Invert) and rotate the image clockwise (Image -> Transform -> Rotate 90 clockwise).

Step 5: That should give you a 256×256 or 128×128 16-greyscale image you can now save out in whatever format you wish. GIMP can save based on the extension you give it when exporting. (File -> Export As… then change the extension to .PNG or .GIF or whatever.)

Tada!

Neat.

Or, I had A.I. write this quick conversion script… It can convert one file at a time, or run it in a directory with .PIX files and it will do them all. It currently only supports the 128×128 16-grey and 256×256 16-grey photos. I recall there was a 64-grey mode, so if I find one of those images, I will update the script to do them, too.

#!/usr/bin/env python3
import sys
import glob
from PIL import Image

def convert_pix(pix_file):
    with open(pix_file, 'rb') as f:
        data = f.read()

    if len(data) == 32768:
        width, height = 256, 256
    elif len(data) == 8192:
        width, height = 128, 128
    else:
        print(f"Invalid file size for {pix_file} (expected 8192 or 32768 bytes)")
        return

    pixels = []
    for byte in data:
        pixels.append(byte >> 4)
        pixels.append(byte & 0x0F)

    # Create image
    img = Image.new('P', (width, height))
    img.putdata(pixels)

    # Rotate right 90 degrees (CW)
    img = img.rotate(-90)

    # Invert colors
    inverted_pixels = [15 - p for p in img.getdata()]
    img.putdata(inverted_pixels)

    # Set greyscale palette
    palette = []
    for i in range(16):
        v = i * 255 // 15
        palette.extend([v, v, v])
    img.putpalette(palette)

    # Save as PNG
    output_file = pix_file.replace('.PIX', '.png').replace('.pix', '.png')
    img.save(output_file)
    print(f"Converted {pix_file} ({width}x{height}) to {output_file}")

def main():
    if len(sys.argv) == 1:
        pix_files = glob.glob('*.PIX') + glob.glob('*.pix')
        if not pix_files:
            print("No .PIX files found in current directory")
            sys.exit(1)
    else:
        pix_files = sys.argv[1:]

    for pix_file in pix_files:
        convert_pix(pix_file)

if __name__ == "__main__":
    main()

You can find it on my GitHub along with documentation on what all it needs to run:

https://github.com/allenhuffman/DS69-PIX-to-PNG

Good luck!

Wanted: disassembly of Sub-Etha Software’s MultiBoot

Updates:

  • 2025-11-20 – Thanks to a comment from Jerry Stratton, I have the start of a disassembly for the “DOS” code which would load at $2600. Article updated with what I have, so far.
  • 2025-11-21 – Updating assembly to include the raw bytes that were used.
  • 2025-11-21 – Updating the assembly again, using a tool John Linville pointed me to.
  • 2025-11-21 – Removing asm from this page and linking to the GitHub link instead.

To this date I still think one of the most useful products Sub-Etha Software ever created was MultiBoot. For CoCo hard drive users of the era, most of us had to boot from a floppy disk. That boot disk would contain the necessary drivers to access the hard drive. With boot files, “one size does not fit all” so most of us had a stack of them — one for running a BBS with serial drivers installed, one with the Sierra Online drivers for playing those games, one with all the graphics drivers for that stuff, etc.

MultiBoot allowed putting multiple boot files on one floppy disk. Type “DOS” and a menu is displayed. Choose the one you want, then that boot file is made active and the process continues.

MultiBoot was written by myself and Sub-Etha co-founder, Terry S. Todd. I wrote the OS-9 frontend in C, and Terry wrote the actual MultiBoot code in RS-DOS assembly. He would then provide that code to me as data, then my C front end program would “install” it by copying it out to the boot sector of the boot disk.

That code looks like this in the MultiBoot source code:

char mb_sec34_1[] = { /* Terry's RS-DOS DOS startup code... 50 bytes */
   79,83,134,13,151,211,204,56,0,221,209,12,211,142,0,234,204,2,0,237,132,
   134,33,214,211,237,2,220,209,237,4,173,159,192,4,109,6,38,8,76,129,61,37,
   221,126,57,0,126,215,9
};

char mb_sec33_15[] = { /* Terry's RS-DOS MultiBoot code V1.12... 512 Bytes */
   142,56,0,16,142,38,0,236,129,237,161,140,57,0,37,247,23,1,152,23,1,171,
   32,32,32,32,32,32,32,32,77,117,108,116,105,66,111,111,116,32,86,49,46,49,
   50,13,32,98,121,32,84,101,114,114,121,32,84,111,100,100,32,38,32,65,108,
   108,101,110,32,72,117,102,102,109,97,110,13,32,32,32,32,32,67,111,112,121,
   114,105,103,104,116,32,40,67,41,32,49,57,57,51,32,98,121,13,32,32,32,32,
   32,32,32,83,117,98,45,69,116,104,97,32,83,111,102,116,119,97,114,101,13,
   0,16,142,5,192,16,159,136,23,1,53,32,32,32,32,32,85,115,101,32,85,80,47,
   68,79,87,78,32,116,111,32,115,99,114,111,108,108,13,91,69,78,84,69,82,93,
   32,83,101,108,101,99,116,115,32,32,32,91,66,82,69,65,75,93,32,81,117,105,
   116,115,0,246,58,235,23,1,16,206,4,164,246,58,255,16,39,0,209,240,58,235,
   193,8,37,2,198,8,52,20,223,136,189,58,208,53,20,48,136,32,51,200,32,90,
   38,238,182,58,234,176,58,235,198,32,61,195,4,163,31,1,134,106,167,132,16,
   190,58,253,49,63,38,5,50,98,22,0,151,173,159,160,0,39,241,198,96,231,132,
   198,255,129,94,39,17,129,10,39,36,129,13,39,62,129,3,38,197,15,113,126,
   140,27,182,58,234,39,187,247,1,85,74,183,58,234,177,58,235,36,3,122,58,
   235,22,255,126,182,58,234,76,177,58,255,36,160,247,1,86,183,58,234,182,
   58,235,139,7,177,58,234,36,228,124,58,235,32,223,246,58,234,92,141,112,
   48,27,52,16,142,0,234,204,2,0,237,132,204,0,1,237,2,204,1,218,237,4,173,
   159,192,4,109,6,38,79,16,174,4,49,168,21,53,64,198,5,166,192,167,160,90,
   38,249,134,3,167,132,173,159,192,4,109,6,38,50,126,38,2,134,87,183,149,
   201,134,16,183,255,34,204,63,0,253,255,188,189,246,82,126,169,40,174,228,
   166,128,39,6,173,159,160,2,32,246,175,228,57,166,128,39,251,173,159,160,
   2,32,246,126,215,9,142,59,0,93,39,236,48,136,32,90,38,250,57,0,0,0,0,0,
   0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,66,0
};

Are there any 6809 disassembler gurus out there that might take this data and reverse it back into 6809 assembly source code?

Sadly, the only source code from Terry that I have found is that for his MultiBasic product. I cannot even find the master disks for his InfoPatch, and I do not think I ever had sources to ShadowBBS, OS9Term or anything else he wrote.

Thank you for your attention to this matter.

Disassemblies

The “DOS” code would load into memory at &H2600. The first two bytes must be “OS”, and indeed that matches the first two bytes in the data. Per a suggestion from Jerry Stratton, I wrote a program to POKE this data into memory, then used EDTASM Z-BUG to disassemble and do a bit of cleanup.

Where the “??” comment is was something Z-BUG could not decode. There is a value of 2 there. No cluck. BUT, I see a reference to DSKCON (C004) so I am betting if I look up how that works some of this code might make sense.

Help is appreciated!

NOTE: This is now updated using the f9dasm disassembler that John Linville pointed me to:

https://github.com/Arakula/f9dasm

Closer!

DOS Assembly (f9dasm)

https://github.com/allenhuffman/SubEthaSoftware/blob/main/OS-9/MultiBoot/asm/dos.asm

Multiboot Assembly (f9dasm)

https://github.com/allenhuffman/SubEthaSoftware/blob/main/OS-9/MultiBoot/asm/multiboot.asm

Updates

Tim Lindner pointed me to his online javascript disassembler. It did a great job right in the web browser:

https://www.macmess.org/follow9.html

Wanted: disassembly of Sub-Etha Software’s OS9Term

OS9Term was an RS-DOS terminal program written by Sub-Etha co-founder Terry S. Todd. It emulated the OS-9 text screen control codes. This not only included things like color, move cursor position, and underline, but also the overlay windows! OS-9 Level 2 “windows” on the text screen were all created by sending a series of escape code bytes. Terry’s program would let you dial in to an OS-9 machine using a modem and actually run text-mode windowing applications. I recall modifying my EthaWin interface with an option so it would not create its own window on startup. This allowed me to run the Towel disk utility remotely if I wanted. (EthaWin supported a mouse, but was designed so all functions – including the menu system – could be operated from the keyboard.)

It was a neat product, though I do not recall us selling very many copies. Too little, too late, I suppose.

The DSK image for this is up on the Color Computer Archive site:

https://colorcomputerarchive.com/repo/Disks/Applications/OS9Term%20%28Sub-Etha%20Software%29%20%28Coco%203%29.zip

As I went to find that link, I see this .zip has a Play Now button ;-) So here is a screenshot:

I would love to have a disassembly of this code, if anyone out there might be able and willing to take on such a task. The code is not all Terry’s. This program uses the bitbanger serial routines written by Ultimaterm’s Ken Johnston. At some point, Terry got in contact with him, and he provided his routines to Terry. Sub-Etha’s launch product, Shadow BBS, used the remote terminal driver written by Ken.

I don’t even know where to begin with a task like this, so I thought I’d post and see if anyone out there had some suggestions.

Thank you for your attention to this matter.

EXEC dispatch table for 6809 assembly

I am writing this so one of the 6809 experts who reads this can chime in and tell me a better way…

Often I post things so they can get in the search engines in case anyone else looks for that topic later. This is one of those.

Using DEF USR is a great way to put up to ten “easy to execute” routines in an assembly language program. Each of those routines can also do different things based on the numeric (or string) parameter passed in to the USR() call.

If you aren’t trying to be that fancy, but do want multiple functions for whatever reason, what methods are there? Please leave a comment with the best ways to call multiple functions using EXEC from Color BASIC.

Dispatch table

One method that comes to mind is using a dispatch table at the start of the machine language program. If the code is built to compile at &H3F00, then doing an EXEC &H3F00 will run that program. If there are more functions, you have to figure out where they are located and provide those address to the user. This is fine, until you make a change to the code and then those locations shift.

Instead, the start of the program could begin with a series of “branch always” instructions. For example:

            org     $7f00

start1 bra install
start2 bra uninstall

The branch always instruction is one byte, and it is followed by a second byte which is how many bytes away the function is. This makes each entry take two bytes. Thus, install is at &H7F00 and uninstall is at &H7F02. A whole series of functions could be done this way, and the user just has to remember which is which — &H7F00, &H7F02, &H7F04, etc. Having every two bytes be an entry makes it easy to remember.

; lwasm dispatch.asm -fbasic -odispatch.bas --map
; a09 -fbasic -odispatch.bas dispatch.asm

ORGADDR equ $3f00 ; Where program loads in memory

org ORGADDR

;------------------------------------------------------------------------------
; Absolute addresses of ROM calls
;------------------------------------------------------------------------------
CHROUT equ $A002

;------------------------------------------------------------------------------
; This code can be called by EXEC/EXEC xxxx.
;------------------------------------------------------------------------------
; Dispatch table at the start of the program.
start1 bra install
start2 bra uninstall

install leax <msginst,pcr ; X points to message
bra print ; print will do the RTS
;rts

uninstall leax <msguninst,pcr ; X points to message
;bra print ; print will do the RTS
;rts

;------------------------------------------------------------------------------
; PRINT subroutine. Prints the 0-terminated string pointed to by X plus CR
;------------------------------------------------------------------------------
print lda ,x+
beq printdone
jsr [CHROUT]
bra print
printdone lda #13
jmp [CHROUT] ; JMP CHROUT will do an rts.
;rts

;------------------------------------------------------------------------------
; Data storage for the string messages
;------------------------------------------------------------------------------
msginst fcc "INSTALLED"
fcb 0

msguninst fcc "UNINSTALLED"
fcb 0

end

One potential issue is that branch can only jump so far. If large functions are being called, you might find they cannot be reached from this dispatch table. One option would be to switch to “long branch”, but then you add more bytes and your dispatch table might be every three bytes – &H7F00, &H7F03, &H7F06, &H7F09, &H7F0C, etc.

That is a fine solution though every 2 may “look” nicer than every 3.

As a workaround, the dispatch table could remain short branches, but they go to a longer one just below it:

            org     $7f00

start1 bra install
start2 bra uninstall

; If a short branch cannot reach, it can call a second long branch:
uninstall lbra realuninstall

Above, perhaps “install” is within reach of the “bra”, but “uninstall” is too far away. Simply make the “bra uninstall” branch to a spot with a long branch. A few more bytes, a few more clock cycles, but now the dispatch table can remain “every 2 bytes”.

But there has to be a better way…

Leave your suggestions in the comments.

Until next time…

Bonus

Here is a BASIC loader for that example. RUN it, then EXEC &H7F00 or &H7F02 and be amazed. (Loader generated using Sean Conner’s a09 assembler.)

10 DATA32,2,32,5,48,140,21,32,3,48,140,26,166,128,39,6,173,159,160,2,32,246,134,13,110,159,160,2,73,78,83,84,65,76,76,69,68,0,85,78,73,78,83,84,65,76,76,69,68,0
20 CLEAR200,16127:FORA=16128TO16177:READB:POKEA,B:NEXT: