Copy Link
Add to Bookmark
Report

Machine language I/O routines

DrWatson's profile picture
Published in 
Commodore64
 · 24 Nov 2022

November 9, 1989
Programmers' Workshop Conference

BASIC programmers are spoiled with their OPEN, CLOSE, PRINT, INPUT and GET instructions. In order to perform these functions, the micro- processor must execute hundreds or even thousands of instructions. So, in this conference paper I'm going to give some routines that are hundreds of instructions long to help you get started doing I/O in machine language. Okay? Well... not exactly. You see, Commodore machine language programmers are just a bit pampered too. The C64 has built in 8K bytes of ROM (read only memory) routines dedicated to performing, primarily, input and output from/to other portions of your computer system. The C128, in native mode, has 12K bytes of ROM routines for this purpose. The I/O routines in this "Kernal" ROM have been made general purpose and simple to use so that ML programmers can quickly and conveniently code up programs that need I/O. It takes only 10 instructions in machine language to set up for and perform an OPEN. Two instructions for CLOSE, one for GET (three if performing a GET# equivalent read from a device), 8 instructions to perform INPUT (10 to do the equivalent of INPUT#) and only five to print a literal string (7 for print# to a device or file).


The Kernal

Incidentally, Commodore official terminology for the kernel of routines for I/O calls it the "Kernal". The misspelling isn't mine. I don't know if it was intentional or a typical programmers' misspelling that caused the Kernal to be "the Kernal" instead of "the kernel". Anyway -- the Kernal is constructed with a set of high level entry pointers in a jump table, all with documented, standardized interfaces. The 'jump table' is standard for all Commodore 8 bit computers - PET, VIC-20, C64, C128 - so this improves portability, promotes use of a library of routines, and simplifies the task of writing assembly language code. By using a jump table for the Kernal routines, it is possible to maintain constant call addresses for the I/O routines even though there may be changes to the routines' location within the ROM. The familiar JSR $FFD2 performs a JSR to the instruction at that address. This is the jump table entry point to the routine to print a character to the current output device. At location $FFD2 there is a JMP instruction that transfers control to the actual routine that does the work. That routine is located at different places in a C64, C128, VIC-20 or PET computer, but the JSR to $FFD2 will always take care of the differences.

Just as call addresses are standard, so are the interfaces to these routines. Obviously there are different requirements for parameters for different functions, but for a given function the Kernal routine setup is the same among different computers, different ROM versions or different uses of the function. Any variations required for a particular machine is handled by the actual function coding and the programmer is relieved of that burden. There is a partial table at the end of this paper giving call addresses and interfaces to a few of the common Kernal routines for illustration and to get you started. For a complete table, refer to a Programmers' Reference Guide. There are good ones for each of the Commodore computers published by Commodore or Compute! Publications. There are a few things that are generally common among all of the Kernal routines, however. All parameters passed to a routine are either implicit or contained in the processor registers. Where the actual parameters wont fit in the three registers then a pointer to the parameters is passed in the registers. Where a function returns a value it is always returned in the processor registers. And if an error can occur which prevents the function from performing its job, then the function returns with the carry flag set and, if possible, an error code in the accumulator. This makes error handling relatively simple using the Kernal routines.

To be more specific, and use some examples for illustration, I'll show two common problems in ML programming with some assembly language code. For simplicity, this is shown in Power Assembler (Buddy) format using + and - as temporary labels for forward or backward branching. Labels used for Kernal functions are those commonly given in literature and shown in the table at the end of this paper. These would be assigned to the actual addresses of the routine jump table entries in equate statements in your assembler code. First, a simple text reader that opens a disk file, prints the text to the screen, then closes the file. This routine includes some user control of the output to pause the display or abort it (see comments in the code).

shflag = $28d           ;Address of byte containing flags for shift type 
;keys. %001=shift, %010=control, %100=logo key
;Shflag is at location $d3 in the C128.
seqread = *
ldx #fname
lda #namend-fname ;length of file name calculated by assembler
jsr setnam ;Kernal routine
;
lda #8 ;Logical file #8
tax ;device #8
tay ;secondary address 8
jsr setlfs ;Kernal routine
;
jsr open ;Kernal routine
bcs error ;see OPEN for possible error returns
;
; The file is now open on the disk drive. Next read the data from it
; and print
;
ldx #8 ;Logical file #
jsr chkin ;Kernal routine
bcs endread ;see CHKIN for possible error returns
readloop = *
jsr chrin ;Kernal routine. Get character from serial bus
bcs endread ;see CHRIN for error returns
jsr chrout ;Kernal routine. Print the character
jsr readst ;Kernal routine. Get the status byte for last I/O
bne endread ;EOF or disk error causes end
jsr stop ;Kernal routine. Check the STOP key
beq endread ;and quit if it is pressed
- lda shflag ;check shift, control or logo key pressed
bne - ;pause while one of them is held down
beq readloop ;unconditional. Continue while not (EOF or STOP)
endread = *
jsr clrchn ;Kernal routine. Clear the bus
lda #8 ;logical file #
jsr close ;Kernal routine.
error = *
nop ;change to BRK for debugging
rts
fname .byte 'testfile' ;name of file to read
namend = * ;for calculating name length

For a second example, I will show how to set up an OPEN to a modem channel. This is a commonly asked question in the Programmers' Workshop among those just getting started with machine language programming. The confusion here is because of the interpretation of what a "name" is for a file being opened. The Kernal OPEN routine uses the "name" to contain the bytes that are used to set up the RS-232 port if the device number is 2, the RS-232 channel. Here is the example.

RS-232 Open is done by setting the "name" of the file to open to be the bytes which set the command and control registers. Then use the normal SETNAM, SETLFS and OPEN Kernal calls. For example, to open the file for 300 baud, full duplex, no handshake, 8 data bits and no parity the control register byte is a 6 and the command register byte is 0. Thus there is a two character "name" and SETNAM would be called as follows:

; OPEN RS-232 channel for I/O 
;
rsreg .byte 6,0 ;the RS-232 control and command register values
rsopen = * ;jsr rsopen returns with LFN 2 open to RS-232 port
;
lda #2 ;Length = 2 bytes
ldx #rsreg
jsr SETNAM ;Kernal call
;
;The RS-232 device number is 2 and there is no significance to the
;secondary address that is used in the setup, so the call to SETLFS
;would look like:
lda #2 ;Logical file #2
tax ;Device #2
tay ;secondary address don't care
jsr SETLFS ;Kernal call

Then a JSR OPEN would complete the process.

SPECIAL CONSIDERATIONS

CHRIN vs GETIN for Reading

Use GETIN to get single characters from the keyboard with no cursor and for RS-232 input. Use CHRIN for serial bus devices (disk) and to perform the equivalent of BASIC's INPUT from the keyboard (with flashing cursor and edit keys active). Never use CHRIN for RS-232 input. It can lock up your program waiting for a carriage return from the RS-232 input and if garbage data is coming in, it can overflow the INPUT buffer (88 characters on the C64, 161 characters on the C128). Using GETIN on disk I/O just wastes time. For serial bus or cassette input the Kernal routine GETIN calls CHRIN, so you might just as well use CHRIN. A JSR to CHRIN or GETIN returns one character for each call. If you are inputing a string or continuous data you will have to store the data away as you read it. Neither of these routines performs any buffering, except that CHRIN buffers a logical screen line starting at $201 in either the C64 or C128 if the default input device (keyboard) is being used.


Serial bus I/O

You can only use the serial bus for one action at a time. So you cannot set up one device for input and another for output. For example, to copy a SEQ file from disk to printer it is necessary to read input data, clear the channel, set up the output channel, send the data, clear the channel then repeat at get data. In ML, with logical file 2 for input from disk and logical file 4 for the printer:

getprint = *          ;DO 
ldx #2 ;Disk drive LFN
jsr CHKIN ;Make disk a talker on the bus
jsr CHRIN ;Get a character
pha ;save character for the moment
jsr READST ;Get disk status
tay ;save status temporarily in .Y
jsr CLRCHN ;Turn off talker (disk drive)
ldx #4 ;Printer LFN
jsr CHKOUT ;Make printer listen
pla ;the character we saved
jsr CHROUT ;print it
jsr CLRCHN ;turn off listener (printer)
cpy #0 ;where we put the disk status earlier
beq getprint ;LOOP until ST 0

A routine such as this will tend to be a bit slow because of the serial bus turn-around on every character. It is more efficient if you BUFFER the transfer. To do that, repeat a CHRIN loop that stores characters in a buffer until EOF is reached or the buffer is full. Then perform the serial bus turnaround and send all the data in the buffer to the printer with a CHROUT loop until the buffer is empty. Then, if the last buffer full sent to the printer was not cut off because of an EOF when the data was read from the disk, go back to the CHRIN loop again until EOF is found. The buffer is simply an area of memory that you set aside to hold the data temporarily and thus may be used for several different things as long as the purpose is temporary storage. A convenient buffer size to use is 256 bytes because you can use indexed addressing mode to access the buffer. For special cases you may use a different buffer size, though. For example, using BURST read on a C128 you may use a buffer size of 254 bytes instead as is done in the program "burst read.sda" uploaded by John L.


"Low level" Kernal routines

There are a number of Kernal routines available to manipulate the serial bus directly. It is best to avoid the use of the low level routines unless you specifically want to manipulate the serial bus directly. Using the low level routines can, and usually does, create compatibility problems between your program and such commonly used devices as fast load cartridges, JiffyDOS, hard disk drives, IEEE disk interfaces, and RAMDOS when used with a REU for a virtual disk (RAMDISK). Sticking to the high level routines discussed in this paper will provide the best compatibility with devices and software wedges that many Commodore computer owners like to use. These are often used routinely by ML programmers thinking that the time savings will be appreciated by the program users. It is a mistake though, because the time savings are negligible at ML speeds and the inconvenience of compatibility problems is death to many otherwise useful programs.


Stopping

In most of these routines I have not mentioned any way to stop what is happening. It is a simple matter to stick a JSR STOP in occasionally at places where it is convenient to code it so that a program user can press the STOP key to halt the program. After a JSR STOP then a BEQ ABORT can be used to clean up the I/O and go back to a higher level control program.


Reading Nuls

Another special consideration is reading nuls which are either a byte with a value of 0 or no byte, depending on where it is read from. When doing a CHRIN from a disk file you will ALWAYS get a data byte. So a data byte value of 0 means that there is actually a byte value 0 at that place in the file. For keyboard input, a GETIN will return a 0 value if no key is pressed. You can check for a 0 in the accumulator and not do anything with the byte if it is 0. For RS-232, however, it is possible to receive no byte in response to a GETIN, or it is possible to receive a 0 value byte. Both cases have the accumulator set to 0 on return from the JSR GETIN. So with RS-232 I/O you should check the RS-232 status register at location 663 ($297) in a C64 or location 2580 ($a14) in a C128 after a GETIN. If bit 3 (AND #$08) is a %1 then there was no data returned from the last RS-232 GETIN call. If this bit is a %0 then the you have valid data. Thus this is a better way to check for valid data than to check for a zero byte after the GETIN call.


RS-232 Transmit

A special consideration in sending data to RS-232 is that the Kernal RS-232 send routines buffer up to 256 bytes. In ML you can easily fill the transmit buffer faster than it can be sent. You cannot overflow the buffer, but you can run into problems due to the time delay. For example, in Punter protocol you send a block of data 254 bytes long then wait for the receiving end to acknowledge receiving it. If you fill the RS-232 transmit buffer then start a timeout for the receiving end to acknowledge it you can get into trouble. At 300 baud it takes about 8 seconds to send the full buffer while at 1200 baud it takes only about 2 seconds. So a time delay that is right for one wont be correct for the other. To avoid this, check bit 0 of the RS-232 interrupt flag register, location 673 ($2a1) in a C64, 2575 ($a0f) in a C128. If it is %1 then data is being sent yet. When the RS-232 transmit buffer is empty this bit will change to a %0. So you could avoid the time delay problem by starting your timeout delay when bit 0 of the interrupt flag register is %0. This bit may also be used to pace output of data sent to another computer screen. This allows the receiving end to pause or abort the printout without having to wait for a full RS-232 transmit buffer of data to get sent. You may have experienced this on a BBS that was not set up for transmit pacing and it is annoying, but easily corrected by proper program design.

Hopefully this paper has taken some of the mystery out of doing I/O from machine language. It looks like a lot of code compared to a simple OPEN 8,8,8,"testfile" like you would use from BASIC, and it is. But once you have done it one time and have an assembler source file with it then it is a simple matter to re-use that code in another program. After writing just a few small programs in assembly language you can build a library of routines that you will use over and over again, and thus make the job easier as you gain experience.

Here is a table of some of the commonly used Kernal routines.

Parameter       SETNAM SETLFS OPEN    CHKIN  CHKOUT CHRIN  GETIN  CHROUT 
--------------- ------ ------ ------ ------ ------ ------ ------ ------
Call address $ffbd $ffba $ffc0 $ffc6 $ffc9 $ffcf $ffe4 $ffd2
Parameters A,X,Y A,X,Y none X X A A A
Reg. affected none none A,X,Y A,X A,X A,X A,X,Y A
Prep routines none none SETNAM OPEN OPEN (OPEN) (OPEN) (OPEN)
SETLFS
Error rtns none none 1,2,4, 3,5,6 3,5,7 none none none
5,6,240

Parameter CLRCHN CLOSE READST STOP PLOT LOAD SAVE
--------------- ------ ------ ------ ------ ------ ------ ------
Call address $ffcc $ffc3 $ffb7 $ffe1 $fff0 $ffd5 $ffd8
Parameters none A none none %c,X,Y A,X,Y A,X,Y
Reg. affected A,X A,X,Y A A,X A,X,Y A,X,Y A,X,Y
Prep routines none none none none none SETLFS SETLFS
SETNAM SETNAM
Error rtns none 0,240 none %z=1 none 0,4,5, 5,8,9
8,9

ERROR RETURNS : ERROR NUMBER IN .A IF CARRY IS SET ON RETURN
0 Terminated by STOP key : 5 Device not present error
1 Too many open files error : 6 Not an input file
2 File open error : 7 Not an output file
3 File not open error : 8 File name error
4 File not found error : 9 Illegal device error
240 RS-232 buffer allocated/de-allocated (top of BASIC memory moved)

In addition to these errors, most I/O operations require reading the I/O status value to determine the I/O condition after the routine is called. Basically, if the routine transfers data or results in serial bus activity, then you should execute a JSR READST to check for the results of the transfer. A non-zero result indicates an error. Refer to BASIC contents of the reserved variable ST to determine the effect of a READST. For serial bus I/O you can simply look at the contents of location $90. This location is used by the Kernal to store the ST value. Here are some brief descriptions of the parameter setup for the Kernal routines listed above:

  • SETNAM: .A=name length, .X=low byte of name location, .Y=high byte
  • SETLFS: .A=logical file #, .X=device, .Y=secondary address
  • CHKIN: .X=logical file #
  • CHKOUT: .X=logical file #
  • CHRIN: Returns character in .A (If input device is the keyboard then operation of CHRIN is similar to the BASIC INPUT statement.)
  • GETIN: Returns character in .A
  • CHROUT: .A=character to send
  • CLOSE: .A=logical file #
  • READST: Returns .A = ST value
  • STOP: Returns with the processor Z flag set if STOP is pressed
  • PLOT: Set carry, call with .Y = column, .X = row to set cursor location Clear carry and call to return current cursor location in .X, .Y
  • LOAD: .A = 0 for LOAD. To load at the saved address, use any non-zero secondary address. If the secondary address is set to 0 then the routine will LOAD the program at the address contained in .X, .Y
  • SAVE: .A = ZP ptr to start of SAVE. .X, .Y = end address + 1 of SAVE.

disclaimer

The above document is the sole work of the author and is for informational and educational purposes. It is intended as a review and should be used as such. I except no money, royalties, or gratuities for its contents. I also will not be liable for misuse or any damages, either direct or consequential, from use of any information found here. ALL INFO IS USE AT YOUR OWN RISK !!!

← previous
next →
loading
sending ...
New to Neperos ? Sign Up for free
download Neperos App from Google Play
install Neperos as PWA

Let's discover also

Recent Articles

Recent Comments

Neperos cookies
This website uses cookies to store your preferences and improve the service. Cookies authorization will allow me and / or my partners to process personal data such as browsing behaviour.

By pressing OK you agree to the Terms of Service and acknowledge the Privacy Policy

By pressing REJECT you will be able to continue to use Neperos (like read articles or write comments) but some important cookies will not be set. This may affect certain features and functions of the platform.
OK
REJECT