Binary File I/O (Part 2 of 2)
Part 1 - Introduction
Part 2 - Learn 82.7% of Assembly Language in About Three Pages
Part 3 - The World Inside a USR() Routine
Part 4 - Implement DPEEK()
Part 5 - Implement DPOKE
Part 6 - Various Bit Manipulations
Part 7 - Convert Integer to Hex String
Part 8 - Convert Integer to Bit String
Part 9 - Memory Copy
Part 10 - Binary File I/O Part 1 (XIO is Broken)
Part 11 - Binary File I/O Part 2 (XIO is Broken)
New XIO in Mac/65 Assembler Code
Many articles on this subject go by a simple route – use BASIC code to set up all the IOCB values for the 7/Get Bytes or 11/Put Bytes commands, and then provide a minimal assembly routine that simply calls the CIO vector. While BASIC mangles the binary read/write functions, XIO itself also is sufficiently broken to justify a complete machine language USR() routine that exercises CIO commands the way they were intended. For example, ICAX values are not always needed or even wanted, but XIO requires the values. In fact, ICAX values are rarely needed outside of the 3/Open command. Similarly, the filespec/buffer is often not necessary.
This routine will perform the same purpose of XIO, but allow a variable number of arguments, adding more arguments only as needed:
Variable number of arguments means this program is organized differently from the prior utilities that copy stack arguments to Page Zero. Since this is just an interface for putting values into the IOCB it doesn't make use of Page Zero beyond returning a status value to BASIC.
0100 ; NXIO.M65 0105 ; 0110 ; NEW CIO/XIO INTERFACE 0115 ; 0120 ; SETUP IOCB WITH THE SUPPLIED 0125 ; ARGUMENTS AND CALL CIO 0130 ; 0135 ; USR 2, 4, 5, or 6 ARGUMENTS: 0140 ; CHANNEL == IOCB CHANEL (LOW BYTE) 0145 ; COMMAND == CIO COMMAND (LOW BYTE) 0150 ; BUF ADR == ADDRESS OF BUFFER 0155 ; BUF LEN == LENGTH OF BUFFER 0160 ; ICAX1 == CIO ICAX1 VALUE (LOW BYTE) 0165 ; ICAX2 == CIO ICAX2 VALUE (LOW BYTE) 0170 ; 0175 ; RETURN VALUE IS CIOV RESULT IN Y REG 0180 ; 0185 ZRET = $D4 ; FR0 $D4/$D5 Return Value 0190 ; 0195 CIOV = $E456 ; CIO Vector 0200 ; 0205 IOCB = $0340 ; Base IO Control Block 0210 ICHID = IOCB+$00 ; Handler ID 0215 ICDNO = IOCB+$01 ; Device number 0220 ICCMD = IOCB+$02 ; ** CIO Command 0225 ICSTA = IOCB+$03 ; CIO Status 0230 ICBAL = IOCB+$04 ; ** Buffer address (low) 0235 ICBAH = IOCB+$05 ; ** Buffer address (high) 0240 ICPTL = IOCB+$06 ; Put char routine (low) 0245 ICPTH = IOCB+$07 ; Put char routine (high) 0250 ICBLL = IOCB+$08 ; ** Buffer length (low) 0255 ICBLH = IOCB+$09 ; ** Buffer length (high) 0260 ICAX1 = IOCB+$0A ; ** Aux Byte 1 0265 ICAX2 = IOCB+$0B ; ** Aux Byte 2 0270 ICAX3 = IOCB+$0C ; Aux Byte 3 0275 ICAX4 = IOCB+$0D ; Aux Byte 4 0280 ICAX5 = IOCB+$0E ; Aux Byte 5 0285 ICAX6 = IOCB+$0F ; Aux Byte 6 0290 ; 0295 .OPT OBJ 0300 ; 0305 *= $9000 ; Arbitrary. this is relocatable 0310 ; 0315 INIT 0320 LDY #$FF ; Make the return 0325 STY ZRET ; value -1 ($FFFF) 0330 STY ZRET+1 ; by default. 0335 ; 0340 PLA ; Get argument count 0345 BEQ BYE ; Shortcut for no args. 0350 ; 0355 TAY 0360 ; 0365 CMP #$01 ; One arg is not enough. 0370 BEQ DISPOSE 0375 ; 0380 CMP #$03 ; Three args is not supported. 0385 BEQ DISPOSE 0390 ; 0395 CMP #$07 ; More than six is not valid. 0400 BCC DO_CHANNEL ; All good. Ready to pull args. 0405 ; 0410 ; Bad arg count. Clean up for exit. 0415 ; 0420 DISPOSE ; Any number of arguments 0425 PLA 0430 PLA 0435 DEY 0440 BNE DISPOSE 0445 RTS ; Abandon ship. 0450 ; 0455 ; Pull channel and multiply times 16 0460 ; 0465 DO_CHANNEL ; Arg 1 = Channel 0470 DEY ; subtract one arg 0475 PLA ; discard high byte 0480 PLA ; Channel number 0485 CMP #$08 ; More than 7 channels 0490 BCS DISPOSE ; is invalid. 0495 ASL A ; * 2 0500 ASL A ; * 4 0505 ASL A ; * 8 0510 ASL A ; * 16 0515 TAX 0520 ; 0525 DO_ICCMD ; Arg 2 = Command 0530 PLA ; discard high byte 0535 PLA ; command byte 0540 STA ICCMD,X ; Store Command in IOCB 0545 DEY ; subtract one arg 0550 BEQ DO_CIO0555 ; 0560 DO_ICBA ; Arg 3 = Buffer Address 0565 PLA ; Address high byte 0570 STA ICBAH,X 0575 PLA ; Address low byte 0580 STA ICBAL,X 0585 DEY ; subtract one arg 0590 ; 0595 DO_ICBL ; Arg 4 = Buffer Length 0600 PLA ; Length high byte 0605 STA ICBLH,X 0610 PLA ; Length low byte 0615 STA ICBLL,X 0620 DEY ; subtract one arg 0625 BEQ DO_CIO 0630 ; 0635 DO_ICAX1 ; Arg 5 = Aux Byte 1 0640 PLA ; discard high byte 0645 PLA ; Aux byte 0650 STA ICAX1,X ; Store AUX1 in IOCB 0655 DEY ; subtract one arg 0660 BEQ DO_CIO 0665 ; 0670 DO_ICAX2 ; Arg 6 = Aux Byte 2 0675 PLA ; discard high byte 0680 PLA ; Aux byte 0685 STA ICAX2,X ; Store AUX2 in IOCB 0690 DEY ; This should be zero args now... 0695 ; 0700 DO_CIO ; IOCB is set, now execute... 0705 STY ZRET ; Clear return value low 0710 STY ZRET+1 ; and high byte. 0715 JSR CIOV ; Engage, Mr Crusher. 0720 BPL BYE ; No error 0725 STY ZRET ; Copy Y to return value 0730 ; 0735 BYE 0740 RTS 0745 ; 0750 .END
The initialization is similar to prior utilities. It begins by setting the return value to a known value ($FFFF) that cannot be returned by a successful exit. Then it pulls the argument count and does a series of value checks to identify any invalid number of arguments. If the code identifies an issue here it branches to cleaning the stack and then exits. One difference in the stack argument management is that this utility does not double the argument count to derive the number of bytes on the stack, because it will not be looping to copy the stack values into Page Zero.
The channel handling is more involved than other arguments:
0455 ; Pull channel and multiply times 16 0460 ; 0465 DO_CHANNEL ; Arg 1 = Channel 0470 DEY ; subtract one arg 0475 PLA ; discard high byte 0480 PLA ; Channel number 0485 CMP #$08 ; More than 7 channels 0490 BCS DISPOSE ; is invalid. 0495 ASL A ; * 2 0500 ASL A ; * 4 0505 ASL A ; * 8 0510 ASL A ; * 16 0515 TAX
The channel is pulled from the low byte of the argument. If the value exceeds the range of available channels, then it diverts to the stack cleanup to dispose of the remaining arguments and exits.
Recall the earlier discussion about identifying the IOCB for a specific channel -- multiply the channel number times 16 and add to $340. Here the code multiplies the channel number by 16 allowing use of the value as an index to load values into the correct IOCB.
The remaining arguments are handled similarly:
0525 DO_ICCMD ; Arg 2 = Command 0530 PLA ; discard high byte 0535 PLA ; command byte 0540 STA ICCMD,X ; Store Command in IOCB 0545 DEY ; subtract one arg 0550 BEQ DO_CIO
The values are pulled from the stack and stored in the corresponding IOCB field. Then the argument counter is decremented. At the end of processing the arguments for command, buffer length, and ICAX1 (arguments 2, 4, and 5) the argument count is tested if it has reached zero. If this occurs then the program skips over the work for processing any subsequent arguments.
Finally, it gets down to business:
0700 DO_CIO ; IOCB is set, now execute... 0705 STY ZRET ; Clear return value low 0710 STY ZRET+1 ; and high byte. 0715 JSR CIOV ; Engage, Mr Crusher. 0720 BPL BYE ; No error 0725 STY ZRET ; Copy Y to return value
Recall that the Y register is used to count arguments and by the time the routine reaches this point the Y register is guaranteed to contain zero. So, this is a convenient source to clear the high byte of the return value for BASIC. Next, the code calls the CIO Vector ($E456). When the CIO routine returns the error code is in the Y register and the utility copies that value to the low byte of the return value.
Let's go over a couple implications when this is used in BASIC:
1. The function accepts an absolute address and a length allowing access to any part of memory. While this is much more flexible than XIO it also means that this routine cannot directly accept a BASIC string. This routine can use a string passed by its address via ADR(). It is also up to the BASIC program to pass the correct length. LEN() is correct only when the string has defined content, so a BASIC program must fill or pad out the string to its expected length.
2. Since this is a USR() routine it is not integrated in BASIC's error handling. Therefore TRAP cannot trap any Input/Output errors. The BASIC program must check the return value of the NXIO routine or use the STATUS command to identify problems.
Testing New XIO
Now that we have some experience using files for binary data we're going to start with something different. The Atari BASIC program below, MAKENXIO.BAS, creates a binary file containing the machine language code for the NXIO routine.
1 REM MAKENXIO.BAS 5 REM CREATE NXIO.BIN FILE 10 OPEN #1,8,0,"H1:NXIO.BIN" 15 FOR I=1 TO 94 20 READ D:PUT #1,D 25 NEXT I 30 FOR I=95 TO 255 35 PUT #1,0 40 NEXT I 45 CLOSE #150 END 21996 REM H1:NXIO.OBJ 21997 REM SIZE = 94 21998 REM START = 36864 21999 REM END = 36957 22000 DATA 160,255,132,212,132,213,104,240 22001 DATA 84,168,201,1,240,8,201,3 22002 DATA 240,4,201,7,144,6,104,104 22003 DATA 136,208,251,96,136,104,104,201 22004 DATA 8,176,243,10,10,10,10,170 22005 DATA 104,104,157,66,3,136,240,34 22006 DATA 104,157,69,3,104,157,68,3 22007 DATA 136,104,157,73,3,104,157,72 22008 DATA 3,136,240,14,104,104,157,74 22009 DATA 3,136,240,6,104,104,157,75 22010 DATA 3,136,132,212,132,213,32,86 22011 DATA 228,16,2,132,212,96 22012 DATA 67,3,133,212,96
This will make managing the utility easier, allowing the test program (and any other BASIC program) to load the utility directly from the file without reading DATA statements. Note that the program purposely pads the output to 255 bytes, so that a BASIC program can use the (broken) XIO command to load the binary data.
Next, is the test program that exercises the features of NXIO. It begins by loading the NXIO machine language routine into a string using XIO. This is acceptable for the tightly confined usage here – the program does only one operation to read a file of 255 bytes. The remaining CIO activity in the program is run by the shiny, new NXIO routine:
100 REM TSTNXIO1.BAS10 5 REM TEST THE NEW XIO USR ROUTINE 110 POKE 82,0:GRAPHICS 0 115 DIM NXIO$(255):NXIO$(255)=" " 120 NXIO=ADR(NXIO$) 125 REM READ NXIO FROM FILE 130 OPEN #1,4,0,"H1:NXIO.BIN" 135 XIO 7,#1,4,0,NXIO$ 140 CLOSE #1 145 REM 150 REM TEST THE BAD ARG EXIT 155 REM TEST BAD ARGS 0160 ? "TESTING BAD ARGUMENTS..." 165 BADARG=USR(NXIO) 170 ? "BAD ARGS 0 = ";BADARG 175 REM TEST BAD ARGS 1 180 BADARG=USR(NXIO,3) 185 ? "BAD ARGS 1 = ";BADARG 190 REM TEST BAD ARGS 3 195 BADARG=USR(NXIO,3,3,32000) 200 ? "BAD ARGS 3 = ";BADARG 205 REM TEST BAD ARGS 7 210 BADARG=USR(NXIO,3,3,32000,2,3,3,3) 215 ? "BAD ARGS 7 = ";BADARG 220 GOSUB 595 225 REM 230 REM DO CIO 3/OPEN TO SET UP 235 REM A GRAPHICS MODE AND RUN 240 REM THE 18/FILL COMMAND.2 45 REM FORCE "S:" CLOSED 250 CLOSE #6 255 REM OPEN AS GR MODE 5, NO WINDOW 260 GROPEN=USR(NXIO,6,3,ADR("S:"),2,12,5) 265 REM SAME AS EARLIER XIO FILL DEMO 270 COLOR 3 275 PLOT 70,45:DRAWTO 50,10 280 DRAWTO 30,10:POSITION 10,45 285 POKE 765,3290 XFILL=USR(NXIO,6,18) 295 GOSUB 600:REM WAIT FOR A KEY 300 REM REPORT THE RESULTS 305 GRAPHICS 0 310 ? "GROPEN = ";GROPEN 315 ? "XFILL = ";XFILL 320 GOSUB 595 325 REM 330 REM GAMES WITH BINARY FILES 335 REM LOAD THE 8 BYTE MEMORY FILE 340 DIM D$($="!!!!!!!!" 345 ? "LOADING 8 BYTE MEMORY.BIN..." 350 OPEN #1,4,0,"H1:MEMORYT0.BIN" 355 XREAD8=USR(NXIO,1,7,ADR(D$), 360 CLOSE #1 365 FOR I=1 TO 8 370 ? ASC(D$(I,I)), 375 NEXT I 380 ? "XREAD8 = ";XREAD8 385 GOSUB 595 390 REM 395 REM SAVE THE ROM CHARACTER SET 400 CR=57 344:REM ROM SET $E000 405 ? "SAVING ROM CHARACTER SET..." 410 OPEN #1,8,0,"H1:CSET.BIN" 415 XSAVE=USR(NXIO,1,11,CR,1024) 420 CLOSE #1 425 ? "XSAVE = ";XSAVE 430 GOSUB 595 435 REM 440 REM GAMES WITH THE BINARY LOAD 445 REM SETUP SCREEN FIRST... 450 GRAPHICS 0:POSITION 0,12 455 SC=PEEK(88)+256*PEEK(89) 460 FOR Y=0 TO 7 465 FOR X=0 TO 31 470 POKE SC+Y*40+X,Y*32+X 475 NEXT X 480 NEXT Y 485 ? "NORMAL CSET DISPLAYED" 490 ? "TO LOAD SOFT SET" 495 GOSUB 595 500 REM 505 REM LOAD THE SOFT SET IN 510 REM FLIPPED HALF SETS 515 CH=36864:REM SOFT SET $9000 520 POKE 756,144 525 OPEN #1,4,0,"H1:CSET.BIN" 530 CSLOAD1=USR(NXIO,1,7,CH+512,512) 535 CSLOAD2=USR(NXIO,1,7,CH,512) 540 CLOSE #1 545 ? "SWAPPED, SOFT CSET CSET DISPLAYED" 550 GOSUB 595 555 REM 560 REM THE END 565 GRAPHICS 0 570 ? "CSLOAD1 = ";CSLOAD1 575 ? "CSLOAD2 = ";CSLOAD2 580 END 585 REM 590 REM WAIT FOR A KEY 595 ? "PRESS A KEY" 600 OPEN #1,4,0,"K:" 605 POKE 764,255 610 GET #1,A 615 CLOSE #1620 RETURN
The program begins by reading the machine language routine via XIO into a string 255 characters long. Yes, the routine is actually only 94 bytes long, so it wastes a bit of space. Such is life when using XIO.
The first round of tests validates the argument management. There is a separate test for each bad argument possibility – 0, 1, 3, and 7 (or greater). Each failure to start results in error code 65535 from NXIO:
The next round of tests uses NXIO with all the supported arguments to open a graphics mode 5 display with no text window. Then it draws a shape and uses NXIO to execute the 18/Fill command:
After the fill completes press a key to continue and then the program prints the NXIO exit codes for the Graphics Open and the Fill:
Press a key again and the program runs the next test which uses 7/Get Characters to read the 8-byte MEMORYT0.BIN file created earlier. After loading the file the program prints the ATASCII codes for the bytes in the string. It should report the values below, and then print the exit code from NXIO for the 8-byte read.:
Press a key after this to run the next test. This will use the 11/Put Characters to save the entire 1,024 bytes of the Atari's ROM character set to a file. Then it will print the exit code from the NXIO routine for the operation:
Press a key to run the next test. This will prepare the screen to demonstrate loading the character set from the file into memory. The characters are displayed in the internal order. Note the order of the characters.
Press a key to continue the test. The program will use 7/Get Characters to load the first 512 bytes from the file into the second 512 bytes of the soft character set in memory, and then it loads the second set of 512 bytes from the file into the first 512 bytes of the soft character set in memory. This effectively swaps the images of the first half of the character set with the second half. Observe the “changed” order of the characters:
Finally, press a key to return the display to the normal character set and the program will display the return codes from NXIO for the loading activities and then it ends:
The tests all work as expected, especially all the uses of 7/Get Characters and 11/Put Characters. So, there is no problem with CIO. The problem really is that BASIC's XIO command unnecessarily manages the interface to CIO commands. Correct use of the CIO commands is so simple and the behavior XIO implements is so involved and complicated that it is difficult to consider it simply an accident.
What could be the reason for XIO's bizarrely over-engineered behavior? Perhaps at the time Atari BASIC was designed there was an expectation that these I/O operations must work in increments no less and no more than 255 bytes. Perhaps a misunderstanding between OS design and BASIC design? Perhaps design requirements were in motion and it was too late to fix the behavior. Truly weird.
Below is a list of the source files and test examples from the New XIO discussion available in the disk image and archive. The files are listed in the order presented during the discussion. The BASIC programs come in the tokenized BASIC format ending in .BAS. Two listings in text format are also provided: Atari ATASCII format as .LIS and unix/linux text format ending in .TLS.
New XIO File List:
MSAVEDAT BASIC program to PRINT eight values to a file.
MSAVEDT0 BASIC program to PUT eight bytes to a file.
MLOADDT0 BASIC program to GET eight bytes from a file.
XIOFILL BASIC program performing the XIO fill command using device “Q:”
MSAVEDT2 BASIC program using XIO for 11/Put Bytes to write a file.
MSAVEDT3 BASIC program using XIO for 11/Put Bytes to write data to a file with a trailing string to identify the source of excess file data.
MLOADDT BASIC program using XIO for 7/Get Bytes.
MLOADDTX BASIC program using XIO for 7/Get Bytes with a trailing string to detect excess data read from the file.
MSAVE512 BASIC program to generate a file containing 512 bytes.
MLOAD512 BASIC program using XIO for 7/Get Bytes attempting to load 512 bytes from a file.
MLOAD8 BASIC program using XIO for 7/Get Bytes attempting to load bytes from a file containing 8 bytes.
NXIO.M65 Saved Mac/65 source
NXIO.L65 Mac/65 source listing
NXIO.T65 Mac/65 source listed to H6: (linux)
NXIO.ASM Mac/65 assembly listing
NXIO.TSM Mac/65 assembly listing to H6: (linux)
NXIO.OBJ Mac/65 assembled machine language program (with load segments)
NXIO.BIN Assembled machine language program without load segments with additional data padded to the end of the file to make it 255 bytes long.
NXIO.DAT LISTed DATA statements for NXIO machine language routine.
MAKENXIO BASIC program to create the BIN file with padding to 255 bytes so the file can be loaded using XIO to read the binary data.
TSTNXIO1 BASIC program testing the NXIO USR() routines for various CIO commands.
ZIP archive of files:
Tar archive of files (remove the .zip after download)
The movie really is over this time. Thanks for playing. Enjoy the new toys. Finally, welcome to the new world of assembly language. Try to think of new ways to accelerate and improve BASIC programs.
- End -
Blessed is the man who walks not in the counsel of the wicked, nor stands in the way of sinners, nor sits in the seat of scoffers;