CBM DOS
The Commodore 8-bit computers (PET, VIC-20, 64, 128, etc.)
did not have a Disk Operating System (DOS). Instead the DOS
resided on a disk drive itself, and the computer simply
sent commands to the disk drive to read, write, scratch
(remove), list, etc. files. Most commonly this was done
over the IEC serial bus (multiple devices can be connected
to it in parallel, but only one device can talk at a time).
(The serial bus on the C64 is (in)famously slow, at around
400 bytes per second. Many techniques exist to increase the
speed.)
This is a work in progress and exists chiefly for my own
reference. Most of the information is obtained from other
resources available online, and some is from my personal
experiments. Caveat emptor.
------------------------------------------------------------
Command channel
------------------------------------------------------------
The command channel is on channel 15. (A channel is the
secondary address of IEC, the primary address being the
device number itself.) This channel is used for sending
commands and receiving error codes.
After each command or file open operation, the command
channel can be read. It is in a CSV format: error number,
error, track, sector. Each field except the second (error)
is a two-digit decimal value. After a successful operation
all values are `00`, and the error is `OK`.
------------------------------------------------------------
Other channels
------------------------------------------------------------
Besides the command channel, channel 0 is used for loading
programs and channel 1 is used for saving programs. If
a file is not a program (PRG), its type can be given
when loading, and it will load as a program, e.g., `load
"file,s", 8` will load a sequential (SEQ) file as a program.
If the file actually is a program, it will be loaded and run
the same as if it were a PRG file.
The other channels (2 through 14) are available for opening
any file type.
------------------------------------------------------------
CBM DOS Commands
------------------------------------------------------------
All commands are sent to the device over the command
channel.
The 1541 (and also 1571 and 1581 and possibly others)
support only a single partition and no subdirectories. Some
commands (particularly the partition and directory commands)
are derived from the programming manual for the CMD HD
drives. The 1541 seems to ignore the path in all commands
that it supports.
* Items in brackets `[]` are optional.
* `n` is the medium number, typically a partition number
(sometimes a drive number in dual-drive devices)
* `path` is an absolute (with a `/` preceding all path
segments) or relative (without a `/` before the first
segment) path to the file. A relative path is relative to
the current directory. The root path is a blank string.
If included in a command, it must be enclosed in `/`
characters.
* `name` is a directory or file name
* `newname` and `oldname` are new and old file names (in
rename and copy commands)
Directory and file names have restrictions:
* Cannot be longer than 16 bytes
* Cannot contain some special characters (`,=?*"`) (perhaps
others)
Also, at least on the CMD HD drives, the command input
buffer is 254 bytes long which limits the total length of a
path name (plus file name) that can be used with it.
Since path segments are separated by `/`, it follows that
directory names cannot contain `/`. But a file name might.
The 1541 certainly allows `/` characters in file names.
Perhaps this is why CMD HD uses the path syntax that it
does? (A directory name might be able to contain a `/` on
the CMD HD, but if you do that you’re gonna have a bad time.
You’ll have to change to that directory, rather than use
absolute paths, to access files or further subdirectories
under it.)
“Variables” such as `n` and `path` are encloded in braces
`{}` to distinguish them from literal text in commands.
Commands are shown here in ASCII; in PETSCII in the default
shift state, lowercase letters appear in uppercase, and
uppercase letters appear as various graphics glyphs (e.g.,
box-drawing lines).
# Format a disk
`n[{n}]:{diskname},{id}`
Note: disk name is limited to 16 characters, and ID must be
exactly 2 characters long.
# Initialize drive
`i[{n}]`
# Make directory
`md[{n}][/{path}/]:{name}`
# Change current directory
`cd[{n}][[/{path}/][:]{name}]`
or
`cd[{n}]_` to go up a directory
Note: `_` is a left arrow glyph in PETSCII.
Also note: each partition has its own current directory, so
changing to a different partition (see below) will place you
in its current directory.
# Remove directory
`rd[{n}]:{name}`
Note: current directory must be in the parent of the
directory to remove (use the “change current directory”
command above), and the directory must be empty.
# Scratch (remove) file
`s[{n}][/{path}/]:{name}`
# Rename file
`r[{n}][/{path}/]:{newfile}=[[{n}][/{path}/]:]{oldname}`
Note: both file locations must be on the same partition.
# Copy file
`c[{n}][/{path}/]:{newfile}=[[{n}][/{path}/]:]{oldname}`
Note: file locations may be on different partitions.
Also note: this is actually a “concatenate” command,
supporting up to 5 files to the right of the `=` separated
by `,`.
# Lock/unlock file
`l[{n}][/{path}/]:{name}`
Note: toggle lock on file
# Change current partition
`cp{n}` or `cP{nn}` (where `nn` is `n` encoded as an 8-bit
value)`
Note: partition number `n` ranges from 1 to 254 (possibly
255, but CMD HD user docs say 254). Partition 0 is a special
system partition which contains a directory of partitions
(partition numbers are shown where file sizes normally are
found in a directory).
------------------------------------------------------------
Reading and writing files
------------------------------------------------------------
Unlike modern operating systems which allow a program to
open a file for read with update and to seek within a file,
CBM DOS treats files more or less as streams of data to read
from start to finish or to write completely from start to
finish (though the append mode allows writing more data at
the end of a file).
Then again, CBM DOS does support one file type (relative)
that allows for arbitrary reads and writes and positioning
the read/write pointer within the file. Unfortunately it’s
record-based with fixed-sized records (the record size is
specified when creating a relative file), but it may be
useful for a database type of application.
Here is a (hopefully complete) list of ways to open a file
with CBM DOS:
* Read (file MUST exist): `...,{type}[,r]`
* Write (file MUST NOT exist): `...,{type},w`
* Overwrite (file MAY exist): `@...,{type},w` (if file does
not exist, this is the same as the “write” method)
* Append (file MAY exist): `...,{type},a`
* Read/write relative (file MAY exist): `...,l,{recsize}`
(where `recsize` is encoded as an 8-bit value, not ASCII
or PETSCII; it must be between 1 and 254 inclusive)
(Only the regular “read” and “write” methods have
restrictions on the existence of a file—the file must exist
for “read” and must not exist for “write”.)
File types (values of `type`):
* Program (PRG): `p`
* Sequential (SEQ): `s`
* User (USR): `u`
* Relative (REL): `l`
* Any: `m`
A few notes:
* File names are omitted above for clarity. Replace `...`
with `[[{n}][/{path}/]:]{name}` for the actual file name
part.
* File type `type` and its leading comma (and also
`recsize` and its leading comma for relative files) can
be omitted if the file already exists; it doesn’t hurt to
always include these, though.
* File type `type` and mode can be swapped for all types
except relative; note that if the type is omitted and
only `w` (write) mode is given and the file does not
exist, a new DEL (deleted) file will be created. In
fact, a new deleted file will be created each time it is
opened, even if one by the same name already exists. Of
course, you can’t open the file after it’s created, but
you can remove (scratch) it. This “feature” can be used
to create placeholder files in a directory listing (they
can be used as visual separators or whatnot), but note
that each deleted file occupies at least one block (and
you can actually write data to the file which can consume
more than one block).
* The special type `m` can be used to open an existing file
of any type (it cannot be used to create a file).
* A “splat” file can be opened only if type `m` is used.
(A splat file is one which the disk drive believes is
already open and may be in an inconsistent state until
it’s closed, so the drive doesn’t normally allow one to
be opened. An asterisk `*` (aka a “splat”) appears before
the file’s type in a directory listing.)
* To position the read/write pointer for an open relative
file, send `p{channel}{record_lo}{record_hi}{offset}`
to the command channel; all variables are encoded as
8-bit values (`channel` is the channel number of the open
relative file, plus 96). Record number and offset are
1-indexed.
# Relative files
Relative files are a somewhat obscure feature. As mentioned
above they may be useful for storing a database with
fixed-size records.
Reading a “normal” (program, sequential, or user) file byte
by byte results in a status (as in the BASIC `ST` special
variable, or the return value from the `READST` ML routine)
value of 64 on the last byte which means “end of file”.
Doing the same with a relative file results in a status of
64 on the last byte of each record. Continuing to read will
read starting at the first byte of the next record.
I found that if one creates at least one record, the DOS
will create records for the remainder of the 254-byte block
it’s in (as many as will fit completely within the block);
reading those other records in the block results in a
single byte value of 0xff and a status of 64. Attempting to
read records past the last block results in a single byte
value of 0x0d (carriage return) and a status of 64 (and the
command channel reports error 50, "record not present").
----------------------------------------------------
The Commodore 64 sets the “end of file” I/O status
flag when the last byte is read rather than when
attempting to read past the end of a file. If you’re
coming from a C background (which had existed for
several years before Commodore sold their first
computer, the Commodore PET, and disk drive) or any
modern language with a compatible file I/O system,
this may look foreign to you. The limitation is
that a file is always at least one byte in size. If
nothing was written to the file it will contain a
single carriage return.
On the other hand, attempting to read from RS-232
serial will report no bytes available if the receive
buffer is empty, so at least there’s that.
----------------------------------------------------
------------------------------------------------------------
Listing a directory
------------------------------------------------------------
To get a directory listing, open a file named
`$[{n}][/{path}/][:{pattern}]` and read its contents until
the end of the file. The pattern `pattern` may contain
a question mark `?` to match any single character in
its position and an asterisk `*` to match zero or more
characters in its position (old disk drives support only one
asterisk while modern drives support more than one).
The directory listing is formatted as a BASIC program which
can be listed with the `LIST` command. The first two bytes
are the load address (always 0x0401). Following that is zero
or more lines. Each line contains a two-byte address of the
next line; a zero address indicates this is the end (the
last byte of a zero next-line address should coincide with
reading the end of the file). After a non-zero address is
a two-byte line number, followed by the line’s contents[1]
and a zero byte (null terminator). All two-byte values
(addresses and line numbers) are little endian (low byte
first).
Lines are formatted as follows:
1. The first line is the directory header. This line uses a
reverse-video character before the header to display it
in reverse video.
2. Each file is listed; the line number is the file size
(in blocks), the name is quoted, and then the type is
last.
3. The last line contains the number of blocks free (again
using the line number) followed by the text `blocks
free.`.
Technically speaking, lines can be arranged in any order in
memory, since next-line addresses are used to point to the
next line as a linked list. However, a well-behaved drive
should always format the lines sequentially in memory so
that a program reading a directory listing can ignore the
exact value of the next-line address (except when it’s zero
for the end of the listing).
The file name in each entry is enclosed in quotes. A file
name may contain shifted-space (0xa0) characters; in that
case the closing quote is placed at the first shifted
space. Anything following a shifted space is ignored when
matching an existing file. (I suspect shifted spaces are
normally used as padding for names shorter than 16 bytes.)
A file name may also contain quote characters. A file entry
therefore must be parsed with care:
1. Read up to and including the first quote character.
2. Read the following 17 bytes.
3. Find the last quote in the string.
4. That is one character past the end of the file name.
If a file name contains a quote after a shifted-space,
I don’t see how such an entry can be parsed, as it’s
indistinguishable from a name with a quote in place of the
shifted space and without a following quote. In that case,
you’re SOL. (Maybe this “feature” can be used to make a file
unloadable by most mortals.)
------------------------------------------------------------
Sources
------------------------------------------------------------
* Commodore 1541[2]
* CMD HD Manual Remaster[3] or CMD Hard Driver Users
Manual[4]
* LOAD"$",8[5]
------------------------------------------------------------
References and Footnotes
------------------------------------------------------------
[1] BASIC programs are tokenized, but for all intents and
purposes directory listings are just text since they
don’t use any BASIC keywords
[2] https://www.c64-wiki.com/wiki/Commodore_1541
[3] https://archive.org/details/cmd-hd-manual-remaster
[4] http://primrosebank.net/computers/pet/documents/CMD-HDD-Manual_OCR.pdf
[5] https://www.pagetable.com/?p=273