Jump to content
  • 0
desertfish

detecting I/O status in assembly?

Question

Hi, when doing I/O via the kernel routines you can get a long way. However one thing that is missing (as far as I know) is a routine to check the I/O status. 
So I have some assembly code that works on the Commodore-64 where it checks  $90  directly which is the documented zero page location for the I/O status flag on the '64. 

I don't think that address is valid on the cx16.  How are you dealing with I/O errors on the Cx16?   

Share this post


Link to post
Share on other sites

19 answers to this question

Recommended Posts

  • 0

Hi.

I've written some routines that read and write sequential disk files in my project X16 Edit.

Basically, I've followed what's said about the relevant Kernal functions in the C64 PRG.

At least when you access a file, there are two types of errors that may occur.

  • I/O errors thrown by the Kernal, for instance error no 5 - Device not present
  • Errors thrown by the "disk drive", for instance error no 62 - File not found

The carry bit is set if the Kernal throws an I/O error at least when calling

  • SETLFS - $ffba
  • OPEN - $ffc0
  • CHKIN - $ffc6
  • CHKOUT - $ffc9

The error number returned in A register.

The meaning of each error is available in the C64 PRG. In my edition (1 st ed, 9th printing, 1987, it's on page 306). The possible errors are:

  1. Too many files open
  2. File already open
  3. File not open
  4. File not found
  5. Device not present
  6. File is not an input file
  7. File is not an output file
  8. File name is missing
  9. Illegal device number

You may get the other kind of error that is returned by the "disk drive" only by reading the status channel.

In basic you would OPEN 15,8,15 and INPUT#15,E,M$. The last error code returned in E and the error message returned in M$.

An assembly solution is a little more involved. You may look at my file handling functions here if it helps:

https://github.com/stefan-b-jakobsson/x16-edit/blob/master/file.inc

 

Share this post


Link to post
Share on other sites
  • 0

I forgot to mention the Kernal function READST in my post from yesterday.

To sum it up, this is what I think you should do to handle I/O errors when reading a file:

  • Call SETNAM to set the file name. This function does not throw I/O errors.
  • Call SETLFS to set file no, device no and command. As I understand, neither this function throws I/O errors
  • Call OPEN to open the file
    • Check carry bit, if set there was an I/O error, and the error number is in A register
    • Call READST to get I/O status returned in A register. I not zero, there was an error. This should not happen on opening a file for reading, but could occur in other I/O operations.
  • Call CHKIN to open a channel for input
    • Check carry bit, as after calling OPEN
    • Call READST as after calling OPEN
  • Call CHRIN to read one character from the device
    • Carry set does not indicate error after calling this function
    • Call READST after every invocation to get I/O status returned in A register. It not zero there was an error or you reached end of file
    • Call CHRIN in a loop until READST returns end of file or an error

My experience is that the above mentioned Kernal errors only occur if there is a problem communicating with the device. For instance Kernal I/O error no 5 - Device not present - will be thrown if a SD card is not mounted.

But the Kernal will say nothing if the error occurs on what used to be the disk drive side in the old days. For example, a file not found error must be checked by reading the disk status. So the last step is to do that, which requires opening another I/O channel:

  • Call SETNAM (name may be empty)
  • Call SETLFS (like OPEN 15,8,15)
  • Call OPEN. Check carry bit and READST for errors as above
  • Call CHKIN to open channel for input. Check carry bit and READST for errors as above
  • Call CHRIN to read status string
  • Call READST and loop back and call CHRIN until READST returns end of file or an error

There are small variances on how to handle errors depending on the type of I/O channel, for instance if it's a RS232 channel instead of a disk file.

One good source for the functioning of the Kernal is here:

https://www.pagetable.com/c64ref/kernal/

It puts the comments on each Kernal function from a handful different books, including the C64 PRG, side by side.

Or in more simple terms. Use the Kernal functions. Even if you find some memory addresses that return the I/O status they might be subject to change.

Edited by Stefan
  • Like 2
  • Thanks 1

Share this post


Link to post
Share on other sites
  • 0
3 hours ago, desertfish said:

@Stefan you are a legend. Thanks.

Thanks. I feel like a beginner, though. Almost every time a read through the documentation, I see something new. Truly managing all errors while communicating on the serial bus is not trivial.

Edited by Stefan
  • Like 1

Share this post


Link to post
Share on other sites
  • 0

Well look at that. With Stefan's help I managed to make a program that displays the directory of the disk in drive 8.  (without overwriting the program)

It works on both the C-64 and the CommanderX16.   It cleanly deals with I/O errors and you can abort it by pressing RUN/STOP.    Fantastic!!

image.png.fb4b32ad9418f13735412b8d0d4ab45d.png

Prog8 source code:

%import textio
%import syslib
%option no_sysinit
%zeropage basicsafe

; This example shows the directory contents of disk drive 8.
; Note: this program is compatible with C64 and CX16.

main {
    sub start() {
        txt.print("directory of disk drive #8:\n\n")
        diskdir(8)
    }

    sub diskdir(ubyte drivenumber) {
        c64.SETNAM(1, "$")
        c64.SETLFS(1, drivenumber, 0)
        void c64.OPEN()          ; open 1,8,0,"$"
        if_cs
            goto io_error
        void c64.CHKIN(1)        ; use #1 as input channel
        if_cs
            goto io_error

        repeat 4 {
            void c64.CHRIN()     ; skip the 4 prologue bytes
        }

        ; while not key pressed / EOF encountered, read data.
        ubyte status = c64.READST()
        while not status {
            txt.print_uw(mkword(c64.CHRIN(), c64.CHRIN()))
            txt.chrout(' ')
            ubyte @zp char
            do {
                char = c64.CHRIN()
                txt.chrout(char)
            } until char==0
            txt.chrout('\n')
            repeat 2 {
                void c64.CHRIN()     ; skip 2 bytes
            }
            status = c64.READST()

            c64.STOP()
            if_nz
                break
        }

io_error:
        status = c64.READST()
        c64.CLOSE(1)
        c64.CLRCHN()        ; restore default i/o devices

        if status and status != 64 {            ; 64=end of file
            txt.print("\ni/o error, status: ")
            txt.print_ub(status)
            txt.chrout('\n')
        }
    }
}
Edited by desertfish
forgot the source code

Share this post


Link to post
Share on other sites
  • 0

Hmm, why is that?  I was thinking if we do that, channel #1 would no longer refer to the io channel that we opened earlier? 

Share this post


Link to post
Share on other sites
  • 0

Think of a stack -- Last In, First Out.  CLRCHN is the opposite of CHKIN/CHKOUT; CLOSE is the opposite of OPEN.  Therefore, we should use this sequence:

  1. OPEN
  2. CHKIN
  3.   ...
  4. CLRCHN (cancels CHKIN)
  5. CLOSE (cancels OPEN)

CLRCHN means: clear (cancel) the current input channel selection, and clear the current output channel selection.

  • Thanks 1

Share this post


Link to post
Share on other sites
  • 0

Hi,

@desertfish I think your program is fine.

CLRCHN resets all I/O channels to their default state. It's not connected to a specific file or channel. I don't think the stack analogy is relevant.

According to the C64 PRG, CLALL closes all files and invokes CLRCHN. You may look how the Kernal does this.

CLALL is at $d8a0. This is what it happens:

  • First it sets memory address $0289 to 0. This is done by the instructions at $d8a0 and $d8a2. $0289 might be an open file counter or a current file index on X16, but I'm not sure.
  • The next instruction is $d8a5, and that is the start of CLRCHN

Consequently, the Kernal seems to run CLRCHN after closing the files.

To simplify clean up after an I/O operation, use CLALL instead of CLOSE+CLRCHN.

Share this post


Link to post
Share on other sites
  • 0

Thank you again Stefan, CLALL looks like a convenient routine indeed.  I can't imagine -for now- situations where I would be dealing with multiple open files at the same time, so I should be safe with that. 

Share this post


Link to post
Share on other sites
  • 0

NO!  DEFINITELY NOT!

$0289 holds the count of open files.
CLALL doesn't close files; it "drops" them -- it makes the computer forget that files still are open!  DOSes aren't told to close their files!

When CLRCHN resets the computer's I/O selections, it tells peripheral devices to reset their channel connections to the computer.  DOSes like to do that before they're told to close a file.

Share this post


Link to post
Share on other sites
  • 0

Hmm. As I said earlier in this thread, the Kernal I/O documentation is quite hard to fully grasp, and sometimes the information in, for instance, C64 PRG might even be misleading.

If you search for examples on the web, CLOSE is normally called before CLRCHN. In fact, I haven't seen those functions called in reverse order.

It's true that CLALL seems to only reset the open file counter, and thereafter run CLRCHN. CLOSE seems to send actual commands over the serial bus. Maybe it's safer to call CLOSE instead of CLALL, even though you could never guess from reading the C64 PRG.

All books I've seen only have partial code examples, never complete code showing how to safely read a file from disk, for instance. It would be nice to understand in more depth how the Kernal handles serial bus communication. If you have the time, the Kernal code is available out there 🙂

Share this post


Link to post
Share on other sites
  • 0

I found a book online that describes the use of CLALL, CLOSE, and CLRCHN in more depth: "Machine language routines for the Commodore 64 and 128" by Todd D. Heimarck and Patrick Parrish, 1987.

On page 63 they talk about CLALL.

As you may see by disassembling the Kernal in the X16 monitor, CLALL only resets the open file counter to 0 and then falls through to CLRCHN. The authors confirm that the routine, despite its name, doesn't close any files, and that the routine is not safe to use when working with files over the serial bus.

If you disassemble the CLOSE routine, you can see that it sends commands over the serial bus to notify the other device. To understand what those commands are, you need to dig deeper into the functioning of the C64 serial bus, which I haven’t done.

https://archive.org/details/Compute_s_Machine_Language_Routines_for_the_Commodore_64_and_128/page/n69/mode/2up

On page 172, there is a code example on how to close a file.

That routine calls CLOSE first and then CLRCHN.

https://archive.org/details/Compute_s_Machine_Language_Routines_for_the_Commodore_64_and_128/page/n177/mode/2up

To wrap it up:

  • Don't use CLALL to close files. Use CLOSE.
  • It’s normal to call CLOSE first and CLRCHN second.

It would be interesting to test how a device reacts to CLOSE if CLRCHN was called first, especially when writing to a file. My limited understanding of CLRCHN is that is sends UNTALK/UNLISTEN to all devices on the bus, and that could affect how a device handles a subsequent CLOSE.

I might do a small test and see if you get file corruption or other problems, or if it works fine either way.

Have a nice weekend.

Share this post


Link to post
Share on other sites
  • 0

A misunderstanding by some people doesn't make it "normal" or proper.

Here's what the BASIC interpreter does:

10 OPEN 1, 8, 2, "0:FILENAME,S,R"
20 INPUT#1, Y$
30 CLOSE 1
  1. calls SETLFS and SETNAM
  2. calls OPEN
  3. calls CHKIN
  4. calls CHRIN
  5. repeats step 4 until a carriage-return is seen
  6. calls CLRCHN
  7. calls CLOSE

BASIC doesn't call CLOSE before it calls CLRCHN!

Edited by Greg King

Share this post


Link to post
Share on other sites
  • 0

I made a small test program that writes to file. One file was closed with CLOSE+CLRCHN and the other with CLRCHN+CLOSE.

Both alternatives seem to work without issues.

It might just be a matter of taste in what order you call the functions. To me it's more logical to call CLRCHN last, as it's not paired to one CHKIN or CHKOUT invocation - it resets all I/O channels. To clarify what I mean: say that you at the same time open one file for reading and another file for writing to copy the content of the first file to the second file. It's more logical to first close those files before you invoke CLRCHN.

There's no reason to say that Heimarck and Parrish misunderstood the Kernal. Reading their book, it's obvious that they knew the Kernal in great detail.

Share this post


Link to post
Share on other sites
  • 0

To me, it's more logical to tell a drive, "stop talking/listenning about the files" before it's told, "close the files".  (You wouldn't tell someone to put a book back on a shelf before that one stopped reading it.)

A better two-file example is this:
A program reads two files together.  It interleaves data from those files.  It reads some data from the first file.  Then, it reads some data from the second file.  Then, it reads some more data from the first file.  Then, it reads some more data from the second file.  Then, it reads some more data from the first file ... and, so on.  The program must use CHKIN calls to switch between the two files.  The files are on separate devices.

If the program tells the first drive to talk about its file, then tells the second drive to talk about its file, then both drives will talk at the same time!  The computer will hear garbage.  Each drive must be told to stop talking, so that the other one can have its turn to talk without interference.  The program really must pair a CLRCHN with each CHKIN.

OPEN and CLOSE are a pair.  The two CHK... functions and CLRCHN actually are a pair!  I recommend that people get into the habit of thinking about them that way.  Then, you won't have trouble when you start to write programs that handle several files together.

Share this post


Link to post
Share on other sites
  • 0

Hmm, now that I think more about your example, I see that it, also, cannot work if the files are on separate devices, and the program wants to change the data before writing it.  If the program told the first drive to talk, and told the second drive to listen, then the first one would send its data directly to the second one.  The program wouldn't get a chance to alter the data.

Another hmm.  I guess that people who are used to PCs will be confused by this discussion.  Well, ...
a Commodore 8-bit system is a network.  The computer, the disk drives, and the printers are independent microcomputer nodes on that network.  The computer is the boss.  But, drives can be told to talk directly to each other.  Drives can talk directly to printers.  They don't need to send their data through the computer.  They usually do go through the computer, but they are able to bypass it.

And, if file I/O switching isn't managed properly, then bypasses or collisions could happen by accident.

Share this post


Link to post
Share on other sites
  • 0

@Greg KingOh man, the flashback

Quote

In later versions of Fast Hack'em, disk copying can be performed without the computer if two Commodore 1541 disk drives are available. The software is loaded with a Commodore 64, the two drive option is be selected which transfers software to the drives' controller memory, and the serial cable can be disconnected from the computer. Any number of copies can be performed as long as neither drive is powered down.

I used that exact feature.  Could play games on the C64 while the two drives were copying a stack of floppies. It was amazing

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Answer this question...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • Create New...

Important Information

Please review our Terms of Use