Over the years I have shared many tidbits about Color BASIC.
This is another one.
A recent post by Juan Castro to the Groups.IO Color Computer mailing list caught my attention, mostly because he called me out by name in the subject line:
As a reminder, Color BASIC allows 1 or 2 character variable names. They must start with a letter (A-Z) and the second character can be either letter (A-Z) or number (A-0). BUT, the BASIC interpreter does let you type longer names for variables, but it only honors the first two characters. Here is a screenshot from a past blog post here (which I’d link to if I was not so lazy):
Color BASIC variables may be very long, but only the first two characters are used.
This is a reminder that, if you try to use variables longer than two characters, you have to make sure you always keep the first two characters unique since “LONGVARIABLE” and “LOST” and “LO” are all the same variable to BASIC.
…but not all variable name limits are the same.
To break the rule I just said, in Color BASIC, some variable names are forbidden. A forbidden variable is one you cannot use because it is already reserved for a keyword or token. For example, FOR is a keyword:roar
FOR I=1 TO 10 PRINT I NEXT I
Because of this, even though BASIC only honors the first two characters of a variable name, you still cannot use “FOR” as a variable.
FOR=42 ?SN ERROR
But you can use “FO”, since that is not long enough to be recognized as a BASIC token or keyword.
FO=42 PRINT FO 42
There are a number of two-character tokens, such as “TO” in the FOR/NEXT statement (“FOR I=1 TO 10”), and “AS” in the Disk BASIC FIELD statement (“FIELD #1,5 AS A$”), as well as “FN” which is used in DEF FN.
AS=42 ?SN ERROR
FN=42 ?SN ERROR
TO=42 ?SN ERROR
This means if you wrote something for Color BASIC or Extended Color BASIC that uses “AS” as a variable, that would not work under Disk Extended Color BASIC.
BASIC ignores spaces
In recent years, someone pointed me to the fact that when scanning a BASIC line (either type in directly or when parsing a line of code in a program), spaces get ignored by the scanner. This means:
N M = 42 PRINT N M 42
That one surprised me when I learned it. This is probably why, when printing two variables, a semicolon is required between them:
N = 10 M = 20 PRINT N;M 10 20
And if you had done this (remember to CLEAR between these tests so variables are erased each time):
N = 10 M = 20 NM = 30 PRINT N M 30 PRINT N;M;N M 10 20 30
By the way, if you have ever wondered about that space printed in front of numeric variables when you do things like “PRINT X”, I covered why this happens in an earlier blog and included a simple patch to BASIC that removes that feature.
How to turn a forbidden variable into a non-forbidden one for fun and profit
Well, Juan Casto showed that using this “BASIC ignores spaces” quirk as a way to use forbidden variables. From his post:
Now it seems obvious. BASIC’s interpreter looks for keywords like “FOR” and will not recognize “F O R” or “FO R” as that token. The detokenizer honors the spaces.
But when it comes to variables, the spaces are ignored by the parser, so “T O” will not match as a token for “TO”, but will be processed as a variable “TO”.
Go figure.
Admittedly, space in two-character variable names look silly, but now I can finally update my old *ALLRAM* BBS to use the variable “TO$” for who a message is to:
FR$="ALLEN HUFFMAN" T O$="JUAN CASTRO" SB$="THAT'S REALLY COOL"
I suspect this was discovered by the early pioneers of BASIC, likely soon after the original Color Computer was released in 1980. If you know of a reference to this behavior from some early newsletter or magazine article, please let me know.
And as to Juan … thanks for sending me down a BASIC rabbit hole again…
Over the years, I have posted about things that fall into the category of “how did this ever work?” I think I need to make an actual WordPress Category on this blog for these items, and I will do so with this post.
Every embedded programming job I have had came with a bunch of pre-existing code that “worked.” There are always new features to be added, and bugs to be fixed, but overall things were already at an “it works, ship it” state.
And every embedded programming job I have had came with code that “works” but decided to not work later, leading me down a rabbit hole of code exploring to understand what it did and why it was suddenly did not.
This was often followed by finding a problem in the code that made me wonder how it ever worked in the first place.
SPI and interrupts on the PIC24 processor
This post is not specifically about SPI and interrupts on the PIC24 processor. I just happened to run into this particular issue in that environment.
On the seven hardware systems I maintain code for, there are various devices such as ADC, DAC, EEPROM, attenuators, phase shifters, frequency synthesizers, etc. that are hooked up to the CPU using I/O, I2C or SPI bus.
In the code main loop, the firmware reads or writes to these devices as needed, such as sampling RF detectors or making adjustments to power control attenuators.
Recently, we were testing a pulse modulation mode (PWM) where the RF signal is turning on and off. When on, the power is measured (detector read) inside an interrupt that was tied to the PWM signal. This code has been in place since before I joined 6+ years ago and, other than some bugs and enhancements along the way, “it works.”
However, this SPI code ended up being the cause of some odd problems that initially looked like I2C communications were failing. Our Windows-based host program that communicated over I2C would start having communication faults with the boards running firmware.
After a few days of Whac-A-Mole(tm) trying to rework things that could be problematic, I finally learned the root cause of the issue:
SPI and interrupts
The main loop was doing SPI operations (in our case, using the CCS PCD compiler and its API calls such as spi_init, spi_xfer, etc.). One of those SPI operations was for reading RF detectors. When PWM mode was enabled, the an interrupt service routine would be enabled and the main loop RF detectors reads would shut off and we would begin reading the RF detectors inside the interrupt routine as each pulse happened.
When a SPI operation happens, the code may need to reconfigure the SPI hardware between MODE 0 and MODE 1 transfers, for example, or to change the baud rate.
If the main code configured the SPI hardware for a specific mode and baud then began doing some SPI transfers and the pulse occurred, code would jump into the interrupt routine which might have to reconfigure the hardware differently and then read the detectors. Upon completion, execution returned back to the main loop and the SPI hardware could be in the wrong mode to complete that transaction.
Bad things would happen.
But why now?
The original code allowed pulsing at 100 Hz to 1000 Hz. This meant than 100 to 1000 times a second there was a chance that the SPI hardware could get messed up by the interrupt code. Yet, if we saw this happen, it was infrequent enough to be noticed.
At some point, I modified the code to support 10,000 Hz. This meant there was now 10,000 times a second that the problem could happen.
Over the years we had seen some issues at 10,000 Hz, including what we thought was RF interference causing communication problems (and solved through a hardware modification). Since this mode was rarely used, the true depth of the issue was never experienced.
“It works.”
Simple solutions…
A very simple solution was added which appears to have eliminated this issue completely. Some functions where added that could be called from the main loop code around any access to the SPI hardware. Here is a pseudo-code example of what I added:
Then, inside the interrupt service routine, that code could simply check that flag and skip reading at that pulse, knowing it would just catch the next one (I said this was the simple fix, not the best fix):
void pwm_isr (void) { if (0 == g_spi_lock) // SPI is free. { // Do SPI stuff… } }
Then all I had to do was add the claim and release around all the non-ISR SPI code…
“And just like that,” the problems all went away. Many of the problems we had been unaware of since some — like doing an EEPROM read — were typically not done while a system was running with RF enabled and pulsing active. Once I learned what the problem was, I could recreate it within seconds just by doing various things that caused SPI activity while in PWM mode. ;-)
That was my quick-and-dirty first, but you may know a better one. If you feel like sharing it in a comment, please do so.
But the question remains…
How did this ever work?
Once the bug was understood, recreating it was simple. Yet, this code has been in use for years and “it worked.”
My next task is going to be reviewing all the other firmware projects and seeing if any of them do any type of SPI or I2C stuff from an interrupt that could mess up similar code happening from the main loop.
And I bet I find some.
In code that “just works” and has been working for years.
Just when I thought I was out, they pull me back in.
In part 3 I showed a simple assembly language routine that would uppercase a string.
In part 5, this routine was made more better by contributions from commenters.
Today, I revisit this code and update it to use “what I now know” (thank you, Sean Conner) about being able to pass strings into a USR function without using VARPTR.
First, here is the code from part 5:
* UCASE.ASM v1.01 * by Allen C. Huffman of Sub-Etha Software * www.subethasoftware.com / alsplace@pobox.com * * 1.01 a bit smaller per Simon Jonassen * * DEFUSRx() uppercase output function * * INPUT: VARPTR of a string * RETURNS: # chars processed * * EXAMPLE: * CLEAR 200,&H3F00 * DEFUSR0=&H3F00 * A$="Print this in uppercase." * PRINT A$ * A=USR0(VARPTR(A$)) * ORGADDR EQU $3f00
opt 6809 * 6809 instructions only opt cd * cycle counting
org ORGADDR
start jsr INTCNV * get passed in value in D tfr d,x * move value (varptr) to X ldy 2,x * load string addr to Y beq null * exit if strlen is 0 ldb ,x * load string len to B ldx #0 * clear X (count of chars conv)
loop lda ,y+ * get next char, inc Y ; lda ,y * load char in A cmpa #'a * compare to lowercase A blt nextch * if less, no conv needed cmpa #'z * compare to lowercase Z bgt nextch * if greater, no conv needed lcase suba #32 * subtract 32 to make uppercase leax 1,x * inc count of chars converted nextch jsr [CHROUT] * call ROM output character routine ; leay 1,y * increment Y pointer cont decb * decrement counter bne loop * not done yet ; beq exit * if 0, go to exit ; bra loop * go to loop
exit tfr x,d * move chars conv count to D jmp GIVABF * return to caller
null ldd #-1 * load -2 as error return jmp GIVABF * return to caller
In the header comment you can see an example of the usage, and that it involved using VARPTR on a string to get the string’s descriptor location in memory, then pass that address into the USR function.
Now that I know we can just pass a string in directly, I thought it would be fun (?) to update this old code to use that method. Here is what I came up with. Note that I changed the “*” comments to “;” since the a09 assembly does not support those. If you wanted to run this in EDTASM, you would have to change those back.
; UCASE3.ASM v1.02 ; by Allen C. Huffman of Sub-Etha Software ; www.subethasoftware.com / alsplace@pobox.com ; ; 1.01 a bit smaller per Simon Jonassen ; 1.02 converted to allow passing a string in to USR ; ; DEFUSRx() uppercase output function ; ; INPUT: string ; RETURNS: # chars converted or -1 if error ; ; EXAMPLE: ; CLEAR 200,&H3F00 ; DEFUSR0=&H3F00 ; A$="Print this in uppercase." ; PRINT A$ ; A=USR0(A$) ; PRINT "CHARS CONVERTED:";A ; A=USR0("This is another test"); ; PRINT "CHARS CONVERTED:";A ; ORGADDR EQU $3f00
start jsr CHKSTR ; ?TM ERROR if not a string. ; X will be VARPTR, B will be string length tstb beq reterror ; exit if strlen is 0 ldy 2,x ; load string addr to Y ldx #0 ; clear X (count of chars conv)
loop lda ,y+ ; get next char, inc Y cmpa #'a ; compare to lowercase A blo nextch ; if less, no conv needed cmpa #'z ; compare to lowercase Z bhi nextch ; if greater, no conv needed suba #32 ; subtract 32 to make uppercase leax 1,x ; inc count of chars converted nextch jsr [CHROUT] ; call ROM output character routine decb ; decrement counter bne loop ; not done yet
tfr x,d ; move chars conv count to D bra return
reterror ldd #-1 ; load -1 as error return jmp GIVABF ; return to caller
Here are the changes… In the original version, I have this:
start jsr INTCNV * get passed in value in D tfr d,x * move value (varptr) to X ldy 2,x * load string addr to Y beq null * exit if strlen is 0 ldb ,x * load string len to B ldx #0 * clear X (count of chars conv)
That first jsr INTCNV expects a number parameter and, if not a number, it exits with ?TM ERROR. If it gets past that, the number is in the D register and it gets transferred over to X. In this case, the number is the value returned by VARPTR:
A=USR0(VARPTR(A$))
That value is the address of the 5-byte string descriptor that contains the address of the actual string data and the length of that data. Y is loaded with 2 bytes in from wherever X points which makes Y contain the address of the string data.
After this is a bug, I think. Looking at the comments, I think that “beq null” should be one line lower, like this:
start jsr INTCNV * get passed in value in D tfr d,x * move value (varptr) to X ldy 2,x * load string addr to Y ldb ,x * load string len to B beq null * exit if strlen is 0 ldx #0 * clear X (count of chars conv)
That way, Y is loaded with the address of the string data, then b is loaded with the length of that data, and the branch-if-equal check is now checking B. If the length is 0, it is an empty string so no processing can be done on it. (That’s a bug, right?)
The new code is this:
start jsr CHKSTR ; ?TM ERROR if not a string. ; X will be VARPTR, B will be string length tstb beq reterror ; exit if strlen is 0 ldy 2,x ; load string addr to Y ldx #0 ; clear X (count of chars conv)
The first line is something I learned from Sean Conner‘s excellent writeup on USR. That is an undocumented ROM call which checks is a variable is a string. If it isn’t, it will return back to BASIC with a ?TM ERROR. By having that check there, if the user tries to pass in a number, that error will be seen. As a bonus, if you try to EXEC that code, that, too, will show ?TM ERROR.
After that, B should be the length of the string so tstb checks that to be 0 (empty string) then the rest of the code is similar.
As I write this, I could have altered the order of my new code to do the tstb/beq after the ldy and then it would be closer to how the original worked. But since the original appears buggy, I won’t worry about that.
Now if I load this and set it up, I should see this:
DEF USR0=&H3F00
A=USR0(42) ?TM ERROR
A=USR0("This is a test") THIS IS A TEST
Also, I notice that the value I return can be -1 if you pass in an empty string…
A=USR0("") OK PRINT A -1
…and if it is non-empty, it is only the count of the characters that had to be converted. So “Hello World” converts the “ello” and “orld” for a return value of 8. It does not touch the uppercase “H” and “W” or the space.
I am not sure that is really useful. The code could be modified to return the length of the string it processed, but at least this way you know that a positive non-zero return value means it did do some work.
What do you use your computer for? [_] Word Processing [_] Businesss [_] Games/Fun [_] Telecom [_] Programming [_] Home Apps. [_] Music/MIDI [_] Graphics
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.
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:
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.
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.
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.
Since this is a symmetrical pattern, if we can figure out how to draw one quadrant, we can draw the others.
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.
“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.)
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.
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:
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
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.
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.
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.
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:
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.
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:
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.
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.)