JonBird
Software for OS9
There has been a fair deal of material written on using OS9, applications and
filters but very little that I have seen (in the Dragon world anyway) about
actually writing software for OS9. I think most people only tend to use it for
Stylograph and such like, because it is a completly different environent to
the Dragon in it's native mode and can to be honest pretty horrendous to get
to grips with on a normal 64K Dragon. Originally I intended to cover this as
part of the stuff I've been writing on connecting PC's/Dragons but it's such a
big subject I thought breaking it out separatly would be better. Therefore,
I'll also cover device drivers and stuff like that in support of the PC thing,
because OS9 gets a lot better and easier to use by the addition of say an 80
column screen and hard disk.
DragonDOS vs OS9
Trying to explain the difference between a Dragon running normal BASIC/DOS and
one running OS9 is difficult if you have not used one or the other. However,
for the programmer the Dragon is ideal, because you get the best of both
worlds, in 'native' mode where you can program and do exactly what you want
with the machine and under OS9 where things are a lot more tightly managed by
the operating system which is a lot more how it is becoming with Windows and
such like.
Essentially, OS9 is composed of a set of 'building blocks' and can easily be
expanded by adding or changing another building block or module. A completly
minmimal setup would therefore consist of the OS9 kernel (called OS9p1 & OS9p2
on a Dragon) which is just a block of core procedures required to manage the
system. It has no idea of keyboards, disks or screens or any input/output come
to that and does not even come with any sort of command interpretor (the shell
utility performs this which is a seperate entity in its own right). In fact on
its own it is pretty much useless on an ordinary home computer. However, OS9
has found its home (as many of the books on it will tell you) in lots of real
time applications, where there is no need for direct human interaction.
However, for most of use some sort of interaction is reqired so you will find
a dedicated module for handling all I/O (called the IO Manager (or IOMAN for
short)) and further modules for dealing with specific types of IO. As an
example, standard OS9 is shipped with two file managers, the Sequential File
Manager (SCFMan) which manages all sequential types of devices, such as the
keyboard, screen and knows all about sequential type peripherals, such as
delete keys, carriage return and other line editing functions. The other file
manager is the Random Block File Manager (RBFMan) which looks after random
block type devices, such as disks. This knows all about what an OS9 disks
looks like, what makes up a file and how to find it. Finally, there are the
drivers, which actually talk to the hardware, and on the Dragon are
specifically designed for the Dragon's hardware. For example, DDISK the Dragon
device driver, knows how to talk to the disk controller chip to read a bit of
the disk. On the other hand, it does not know what a file is. Right at the
bottom of the change are the device descriptors, which are basicly blocks of
memory to tell the system, this is disk drive 0, it uses DDISK to talk to the
disk controller, and RBFMan is it's file manager. A common data flow would
therefore look something like:
Your OS9 Program says:
'read 100 bytes from path number n'
Which is passed to IOMAN as:
'read 100 bytes from path number n'
which determines that RBFMan is responsible for path n:
Which is passed to RBFMan as:
'read 100 bytes from path number n'
who knows all about what path n is, and where to get those
100 bytes from:
Which is then passed to DDISk as:
'read sector m'
which actually gets the disk controller to read the data
This collection of modules forms, in equivalent Dragon terms the BASIC & DOS
ROM starting at 32768 ($8000) onwards. On its own, they provide little more
than procedures and subroutines for using your machine.
In order to utilise these routines, and actually get your machine doing some
useful work, some sort of user friendly interface is needed to talk to the
user, under OS9 a program called Shell is provided to do this, and the BASIC
interpretor provides this facility under normal DragonDOS mode. They both work
in a similar manner: each has a set of internal commands which it understands
and can execute, and each has the ability to run external programs. For
example, under Dragon BASIC the line:
PRINT "HELLO WORLD"
is effectivly running at internal program within the interpretor called PRINT.
Likewise with the OS9 Shell:
chd /d0
is understood by the Shell to mean 'change directory to drive D0' and runs the
internal code required to perform this operation.
In order to run a program external to the BASIC interpretor, you need to tell
it explicitly, as in:
RUN "MYPROG"
Tells it that MYPROG.BAS is not part of the on board ROM, and that it has to
go away and fetch it from somewhere else (in this case the disk). The Shell
works slightly differently, in that any command you type which is not
recognized by the Shell itself means that is will go away and attempt to find
it externally. For example:
dir /d1
The DIR command is not recognized by the Shell, therefore it automatically
attempts to load and run it from elsewhere. The Shell itself contains
relativly few in-built commands (unlike the BASIC interpretor) and so for the
majority of things will be forcing the execution of an external program.
One of the utility programs shipped with Dragon OS9 is MDIR which simply
provides a list of things in memory, or a very simplified OS9 memory map. When
I run it on my system I get something like the following:
Module Directory at 10:47:24
ADDR SIZE TY RV AT UC NAME
---- ---- -- -- -- -- ---------
F05E 7E7 C1 1 r OS9
F853 4FC C1 1 r 1 OS9p2
FD4F 2E C0 1 r 1 Init
FD7D 182 C1 1 r 1 BooT
BF00 122 C1 4 r 1 SYSGO
C022 101 C1 1 r 1 Clock
C123 6E7 C1 1 r 1 IOMan
C80A CF5 D1 1 r 1 Rbf
D4FF 40E D1 1 r 2 Scf
D90D 1FF D1 1 r PipeMan
DB0C 5E2 E1 7 r Ddisk
E0EE 3AF E1 6 r 2 Kbvdio
E49D 8D E1 1 r Printer
E52A 28 E1 1 r Piper
E552 2E F1 1 r D0
E580 2E F1 2 r D1
E5AE 3C F1 1 r 2 Term
E5EA 3A F1 1 r P
E624 26 F1 1 r Pipe
E64A 4FA 11 1 r 2 Shell
EB44 121 E1 3 r 1 Rdisk
EC65 2E F1 1 r 1 R0
EC93 160 E1 1 r PC_Disk
EDF3 30 F1 1 r H0
5A00 1A6 11 1 r 1 Mdir
The majority of these modules are the drivers, managers and general routines
that make up the 'core' of the OS9 system. In fact, the only programs as such
are the Shell, and the MDIR program itself. One of the things to note, is that
if you run MDIR on your OS9 system, it would almost certainly look different:
not only would you probably have different drivers, they would almost
certainly appear in different areas of memory. This is one of the key
differences between OS9 and normal Dragon BASIC mode, that any program written
for OS9 does not know where it will be in memory and therefore has to be
written to be run from any location in memory. One of the side effects of this
is that code modules in OS9 will probably tend to execute slower that a
DragonDOS counterpart. Part of this is also due to the fact that within the
Dragon BASIC/DOS ROM routines always lie at fixed addresses, and can be called
quickly. OS9 programs have to perform more complex indexing operations to find
routines. Therefore, OS9 always seems considerably more slower and therefore
more painful to work with than DragonDOS.
To see this, and some of the pros and cons of writing programs for each
system, here is a couple of 'hello world' routines for each system. Firstly,
the Dragon assembler version - this assumes DASM to be the assembler:
10 EXEC &H6C00
20 ALL
30 @MSG FCC 0,"HELLO WORLD",0
40 @START LDX #@MSG
50 JSR $90E5
60 RTS
70 END @START
80 EXEC
It is a fairly simple piece of code, and also fairly easy to write, assemble
and get running. Firstly, DASM is loaded from disk, you write the code much
like a BASIC program using all the normal BASIC editor functions then just run
it. The program itself makes use of a standard ROM routine, which prints a
string pointed to by the X register. The 'END' statement at line 70 puts the
starting address into the default EXEC location, which is then called by line
80.
Onto the OS9 version then:
nam WORLD
ttl Hello World program
ifp1
use /d0/defs/os9defs
endc
*Data area
org 0
stack rmb 200
datsiz equ .
*Module header
type equ PRGRM+OBJCT
revs equ REENT+1
mod length,name,type,revs,start,datsiz
name fcs /WORLD/
msg fcc /Hello World/
fcb 13
*main code
start leax msg,pcr
ldy #12
lda #1
os9 i$writln
bcs error
clrb
error os9 f$exit
emod
length equ *
One of the first problems is actually getting an environment to write this in.
Dragon OS9 comes with an edit command, which in all honestly is complete
rubbish and I'd suggest you use Stylo or another editor. I use a program
called 'ed' which is based on the Unix 'vi' editor. Once you've written it,
saved it to disk, you can assemble it using the OS9 assembler:
asm world #20k
Assumes you've saved the file as world. Assuming all goes well, you will end
up with a file called 'world' in your default execution directory (typically
/d0/cmds). However, it is the norm for OS9 (I have found anyway) that things
are not often that straightforward. Firstly, you may find you don't actually
have a copy of the 'asm' program, and even if you do the program you have just
written requires an additional file (called up by the 'use /d0/defs/os9defs'
statement) which may or may not be on your disk. Either way, you will probably
end up searching through a variety of disks in order to get the files required
to the right place. Once you have, and a great deal of disk chugging later
with any luck, simply typing 'world' at the OS9 shell prompt, should once
again after a bit of disk chugging display the message 'hello world' on your
screen.
So how does it all work, and is it all worth it. Firstly, the NAM and TTL
directives just tell the assembler the name and title of your program and are
not strictly required. Next, there is an assembler directive to include the
file /d0/defs/os9defs but only on pass one (the IFP1 condition) of the
assembly. This file contains a list of definitions about OS9 and is almost
always required for any OS9 module. Whilst you can get away without it, for
general neatness and code readability it's a good idea to use it. For example,
in our simple program, the words PRGRM, OBJCT, i$write and f$exit are all
definitions from os9defs.
Next the program's data area is defined, which as a minimum should define at
least 200 bytes for the stack (I'll cover data areas later). Following this,
information for the programs module header is defined. In OS9, each program
unit or module must conform to a specific format in order for OS9 to recognize
it and successfully execute it. In particular, it contains information about
the execution address, how much data size is required and also a checksum of
the whole module. All this minimizes the risk of trying to run corrupted or
invalid code. The 'mod' assembler directive tells the assembler to form this
header using the information provided. This requires the overall length of the
module then a pointer to the name of the module. Next are two equates defined
as TYPE and REVS. The TYPE equate is a single byte defining the module type
and language and is preset to a normal program module (PRGRM) and 6809-object
code (OBJCT). The REVS equate is a single byte defining the module attributes
and revision level. It has an attribute of sharable (REENT) and a revision
level of 1. The final two parameters are the start address and overall module
data size.
Following the module header is the module name. The FCS directive is much like
the FCC directive except the top bit of the last character is the string is
set. This is often used by OS9 to indicate the end of a string. Finally, the
main code for the program occurs.
The code make use of the OS9 system call i$writln. In order to make a system
call, the assembler directive 'os9' is used followed by the call name. All the
valid OS9 call names are defined in the os9defs file. OS9 system calls are
made through one of the software interrupts, in this case SWI2 followed by a
unique code identifying the call number. Therefore, the assembler directive
'os9 i$writln' translates into 'SWI2 $8C' where $8C is the i$writln code.
This call is much like the ROM call at $90E5, where the X register points to
the string to display. In addition, however the Y register should also contain
the maximum number of characters to display and the A register should contain
the path number. A path number of 1 refers to the standard output - the
display. The call will then display the required number of characters, or up
to the first carriage return - whichever occurs sooner. On completion of the
call, the carry bit is set if an error has occured with the error number in
the B register. Finally, the routine is terminated with a call to the F$EXIT
routine not an RTS instruction.
Once the code is complete, the EMOD statement indicates that this is the end
of the module, and generates the module's checksum and then the length label
is calculated.
This program also gives an example writing position independent code ie. code
that can be executed anywhere in memory. Note how the X register in the os9
example is set to point to the string compared to the DragonDOS one:
DDOS: LDX #@MSG - absolute address of MSG
OS9: leax msg,pcr - relative address of MSG
The OS9 variant sets X to point to the MSG label relative to the current
program counter ie. whereever you are in memory. The DragonDOS one specifies
the absolute address in memory. This means that with the DragonDOS code you
are forced to have your program at this location in order for it to work. Of
course, you could replace your DragonDOS one with the same instruction as the
OS9 one and make it position independent, but with OS9 you must use this
format or it will not work.
OS9 Advantages
From the last couple of articles, we have created two comparable programs
under Dragon BASIC and OS9 - the now famous 'Hello World' program. However,
the steps to create the OS9 one are significantly more complex than the
DragonDOS counterpart. We have already seen one of the OS9 program's
advantages - that of position independent code, though with a bit of
forethought the Dragon BASIC one can also be made position independent. Here
are a few other advantages of the OS9 variant.
Suppose you wanted to print the words 'Hello World' on the printer. This can
be accomplished fairly simply under both OS9 and Dragon BASIC:
Dragon BASIC: POKE 111,254:EXEC
OS9: world >/p
The Dragon BASIC variant simply sets the default IO location to 254 (-2) the
printer. The OS9 one utilises the shell re-direction operator (>) to send the
output to the printer. So far so good. What if you wanted to send this message
to a file for example. With standard DragonDOS, there is no easy way - later
DOS variants can use the OPEN# format:
OPEN #1,"O","TEST.DAT"
POKE 111,1:EXEC
CLOSE #1
Once again the OS9 variant is pretty much straightforward:
world >test.dat
Suppose then you have added a custom device to your system, an extra parallel
port for example for a second printer. Under OS9 all you need to do is create
a new device driver/descriptor and call it say P1. The world program can
automatically be used with your new device:
world >/p1
The OS9 world program can then theorhetically be run on any OS9 system and
make use of any device which is implemented on that machine.
The other 'selling point' of OS9 is it's multi-tasking ability - ie. run more
than one program at once. This is all handled by the OS9 kernel routines, and
you do not need to worry about it when writing code, except to ensure your
programs are position independent, and re-entrant. A re-entrant program is all
to do with how any data your program utilises is managed and means that one
copy of a program in memory can be running a number of times with a different
set of data.
OS9 Data Storage
When you write your OS9 program, you should allocate all your data areas using
rmb directives starting at 0 eg.
*Data area
org 0 specify starting at 0
length rmb 2 data items for
name rmb 20 the program
next rmb 2
stack rmb 200
datsiz equ .
Your program's stack is also part of this data area, so you should allocate a
sufficient area for this - 200 bytes is a recommended minimum. The datsiz
label gives the total data size required for the program, and is used as part
of the module header created by the MOD assembler directive:
mod length,name,type,revs,start,datsiz
When OS9 starts your program, it sets up some of the CPU registers to provide
information about your data area-
| |
| Parameter Area |
------------------- -- X, S
| |
| |
| Data Area |
| |
-------------------
| Direct Page |
------------------- -- U, DP (LS address)
Firstly, the U and DP registers point to the start of your data area, with the
stack pointer set to the top of your data area. Essentially then, you just
need to reference the U register when accessing your data variables within
your program:
sty length,u stores Y into 'length'
ldx next,u loads X from 'next'
However, any data items stored within the first 256 bytes of your data area
can be accessed more quickly and efficiently by making use of the DP register.
This is an 8 bit register which contains the upper 8 bits of an address. In
order to specify DP addressing with the OS9 assembler simply supply the data
label:
sty length use DP addressing to
ldx next access data items
Using the data declarations used earlier:
length lies at offset 0 ($00).
next lies at offset 22 ($16).
The assembler translates this into:
STY $00
LDX $16
instructing the processor to use the DP register to provide the upper part of
the address. Therefore, if DP=$20 this would become:
STY $2000
LDX $2016
Not only does this form of addressing take up less storage space, it is also
quicker to execute.
You will still need to use the U register to set up pointers to data buffers
since you cannot use the DP register for this:
leax name,u X=start of name item
There is an important difference between the way the OS9 assembler and
DragonDOS assemblers (DASM & DREAM) use the DP register. By default, the OS9
assembler will use the DP register if you just enter something like:
ldx next -> LDX $16
However, DragonDOS assemblers will automatically use extended addressing so
assuming @NEXT lies at address $5020.
LDX #@NEXT -> LDX $5020
The assembler directive '>' is used to set extended addressing under OS9 ie.
ldx >$16 -> LDX $0016
and co-incidently is used to force DP addressing under DragonDOS ie.
ldx >$F0 -> LDX $F0
The DP register is nearly always set to 0 under DragonDOS so you can use it to
quickly access memory locations 0-255.
So, just by the kernel setting the U and DP registers to different positions
in memory the same piece of code need only be resident in memory once but can
be running in different tasks with different data areas. The REENT bit set in
the module header is used to tell OS9 that our program supports this - if you
recall:
*Module header
type equ PRGRM+OBJCT
revs equ REENT+1
mod length,name,type,revs,start,datsiz
name fcs /WORLD/
msg fcc /Hello World/
fcb 13
The parameter area (pointed to by the X register on program entry) is utilised
to pass data to a program. If it has been started from the shell (command
line) any text following the program name will be stored here terminated by a
carriage return ie.
OS9:world this is a parameter
then X would point to a block of memory containing the ASCII string:
this is a parameter[CR]
Other Programming Tips
Generally, thats about all there is to writing OS9 programs. Remember, that an
OS9 process should exit by calling os9 f$exit (with the B register containing
any error code you wish to return, or 0 for none) instead of rts. The OS9
System Programmers Manual which you should have with your copy of OS9 contains
all the system calls available. However, its worth mentioning a bit about
standard IO paths.
We have already seen standard output used with the Hello World program:
ldy #12
lda #1 standard output
os9 i$writln
bcs error
On entry to any OS9 program 3 file paths are open:
0 = Standard Input (keyboard)
1 = Standard Output (screen)
2 = Standard Error (screen)
These may or may not correspond to screen or keyboard depending on whether the
shell or other program has re-directed them ie.
world >/p
forces standard output to be the printer. This means that for the duration of
your program, path number 1 is still valid but points to the printer instead.
This is the premise of a lot of OS9 utilities called filters which typically
perform some kind of modification to incoming data and write the modified data
out. An example might be a program to add line feeds to a file in preparation
to porting it to another environment which requires CR/LF terminated strings
as opposed to CR which OS9 uses. The basis of the filter might be:
loop
leax buffer,u point to buffer
ldy #1 1 char to read
clra from stdin
os9 i$read read byte
bcs error handle error
inca set to stdout
os9 i$write write byte
bcs error
ldb ,x was it a CR
cmpb #13
bne loop no, loop for next
ldb #10 write an LF
stb ,x to stdout
os9 i$write
bcs error
bra loop
error cmpb #E$EOF check for EOF
bne term if not, exit with error
clrb else quit ok
term os9 f$exit
Here we make use of the i$read, i$write calls which read and write 'raw' data.
A byte is read from stdin and then written out again to stdout. If it was a CR
then an LF is also written to stdout. The program will terminate when an error
condition occurs. If all the data is exhausted, this error will be EOF on the
read which is therefore not treated as an error and causes the program to exit
without error. This is the case for all filters. Assuming the program is
assembled to the file 'addlf' an example usage could be with the stylograph
mail merge utility (mm):
mm my_doc ! addlf >/d1/my_doc.out
Here the Stylograph file 'my_doc' is output by mailmerge and piped into our
addlf utility, so stdin is redirected here from the pipe. The resulting output
(stdout) is then sent to the file 'my_doc.out'.
This filter isn't very efficient, because it carries the overhead of calling
i$read/i$write for every byte. If you recall, an OS9 system call translates
into an SWI2 (software interrupt) call. This results in a lot of processing,
and also the stacking/de-stacking of all the registers required for an SWI
call. It would be far better to read the data in larger chunks and process it
in a separate loop.
Note that any registers not updated explicity for output of a system call will
remain unchanged from when the call was made eg.
i$read: Entry: X = Addr of data
Y = Bytes to read
A = Path number
Exit: Y = Bytes actually read
ALL OTHER REGISTERS UNCHANGED
Device Drivers
One of the things which became apparant after the infamous 'world' routine was
that this simple program could make use of any OS9 system peripherals provided
the drivers were there for it to use. This is also true of most OS9 software -
if you have the drivers any software can use it. For example, if you added a
hard drive to your system and called it H0, you could quite easily store
Stylograph on it, save your data on it, run the C compiler not to mention our
'Hello World' program. Unfortunatly, this isn't the case with DragonDOS which
is more or less tied to the pre-set Dragon hardware. So if you are planning to
add ram discs, hard discs etc. then OS9 is by far the easiest platform to
write software for. You will also probably find that writing software becomes
that much easier for OS9 once you have access to faster disk drives and maybe
an 80 column screen. It's by no means impossible to write stuff for DragonDOS
- just a lot harder thats all and I'll touch on this a bit later.
The key to this flexibility really comes down to device drivers and
descriptors on an OS9 system. Generally, any device you add will fall under
the category of disk - in which case an RBF device driver is required or
sequential (serial port, printer etc.) in which case an SCF device driver is
required.
Device drivers are just like any other OS9 program, they have a module header,
data area and code to perform to necessary operations. Here is a rough outline
of a device driver which is called DEVx:
nam DEVx
ttl DEVx device driver
ifp1
use /h0/defs/os9defs
use /h0/defs/iodefs
use /h0/defs/scfdefs
endc
*data
org V.SCF
PST equ .
type equ DRIVR+OBJCT
revs equ REENT+1
mod PEND,PNAM,type,revs,PENT,PST
PNAM fcs /DEVx/
PENT lbra INIT
lbra READ
lbra WRITE
lbra GETSTA
lbra PUTSTA
lbra TERM
INIT .
READ .
WRITE .
GETSTA .
PUTSTA .
TERM .
emod
PEND equ *
It follows the standard module structure, first the name and title of the
module NAM and TTL. Then the 'use' files are given. As well as os9defs we
include iodefs which contains i/o specific definitions, and because this just
happens to be an SCF device, scfdefs are included also. Next comes the data
area, however because the file managers (RBF or SCF) also require some data,
you should not start your data area off at 0 but the V.SCF equate (or DRVBEG
for RBF devices). No stack space is required here as the stack used will be
the one from the calling program. The PST label marks the datasize of the
module, and then the module header is given. Notice that the PRGRM equate is
replaced by the DRIVR equate indicating this is a device driver.
The PENT label marks the start of the driver. All OS9 drivers have 5 entry
points at the module start and LBRA instructions should always be used to call
these routines which are as follows:
INIT - called once on module start. Include any initialisation code required
to set your device up.
READ - called to read an 'item' of data from the device. Depending on the file
manager an 'item' can be anything from 1 character (SCF devices) to a disk
sector (RBF devices).
WRITE - called to write an 'item' of data to the device.
GETSTA - called to perform miscellaneous IO status routines. Since all IO
devices have different capabilities this provides a method to perform
operations which generally return some sort of status back to the caller - for
example the End Of File function (EOF) is implemented via a GetStat call.
SETSTA - similar to GetSta, but generally used to set some status information
on the device.
TERM - called once when the module is about to be removed from memory. Include
any de-allocation of memory/clean up routines etc. here.
Prior to showing a couple of example drivers, there are a few data structures
you need to be aware of when dealing with device drivers. Firstly, there is
your static storage, which can be thought of in similar terms to the data area
of a normal module. The static storage area for your device driver starts at
the V.SCF (or DRVBEG) position given in the previous header file, and
similarly to a normal OS9 program, the U register always points to this area.
Note, that unlike a module's data area, you should not use DP addressing on
data items. So, if you declare a variable as such:
org V.SCF
flag rmb 1
it can be accessed in any of the 5 procedures as:
lda flag,u
In addition to your own data held here, the system also stores some of it's
own static data just prior to the V.SCF (or DRVBEG) label which you can also
access. This consists of a 'common' area followed by a file manager specific
area. The exact format of these are listed in the System Programmers Guide
(and also in the iodefs,scf/rbfdefs files) but one of these items held in the
common area which the example drivers will make use of is named V.Port and
holds the address of the physical device in memory. How this gets set up, I
will cover later but suffice to say it enables the driver to access a physical
device in memory without having to know where it is exactly. As an example, if
you had an SCF device driver to talk to MC6821 PIAs and you had 4 of these
PIAs in your system you would only need 1 copy of the driver loaded - the
system would ensure V.Port is setup correctly depending on which PIA is being
accessed:
lda #$FF A=255
ldx V.Port,u port address
sta 1,x port address+1=255
The other data block you may need to be aware of is called the path
descriptor. This is a unique set of data for each open file or device in the
system. You cannot allocate your own data items here, but you can read & write
to data items here. Generally, the path descriptor holds information relevent
to the current open file or device (such as path numbers, file lengths etc.)
rather than constant device characteristics (port address etc.). Again, there
is a common part of the path descriptor, and then a file manager specific
area. The format of path descriptors is also listed in the System Programmers
Guide and can also be found in the iodefs,scf/rbfdefs files. Initially, you
will not probably need to utilise anything in the path descriptor, but if you
start writing more complex drivers you almost certainly will - generally the
file manager will provide most of the path descriptor management.
The path descriptor is pointed to by the Y register given in the READ, WRITE,
GETSTA & SETSTA procedures (remember, it only exists for an open file so will
not be accessable by the INIT & TERM routines) and as such can be accessed as
follows:
lda PD.PD,y A=path number from path descriptor
Last time I covered general device drivers. Next, are 2 example drivers, one
an SCF device, the other an RBF device.
SCF Device Driver
This device driver utilises an MC6821 PIA to perform character driven IO.
The first section is the common module header format:
*******************************************
* PIA21
* Device Driver for MC6821 PIAs
*
* By J.Bird (c) 1992
*
* Uses Side B of MC6821 PIA
* CA2 & CB2 Control Strobe lines
* Non-interrupt driven
*
nam PIA21
ttl MC6821 PIA Device Driver
ifp1
use /h0/defs/os9defs
use /h0/defs/iodefs
use /h0/defs/scfdefs
endc
***********************
* Edition History
*
* # date Comments
* - -------- -------------------------------
* 1 05.05.92 Driver first written. JRB
* 2 25.03.95 Update for Parallel Dragon link. JRB.
Revision equ 2
PBCREG equ %00101101
PBOUTP equ %11111111
PBINP equ %00000000
org V.SCF
PST equ .
mod PEND,PNAM,Drivr+Objct,Reent+Revision,PENT,PST
fcb READ.+WRITE.
PNAM fcs /PIA21/
PENT lbra INIT
lbra READ
lbra WRITE
lbra GETSTA
lbra PUTSTA
lbra TERM
This defines an SCF man device called PIA21. On a sideline note, you may have
noticed the revision number as 2, and this is built into the module header.
This will ensure that if this module is loaded into memory and revision 1 is
also in memory (as part of the OS9 boot load for example) that revision 2 will
be used. Subsequently, a revision 3 would take precedence over 2 etc. etc.
The following two routines will be used throughout the code to configure the
port for either input or output. Note the use of the V.Port address from the
static storage to find the IO port's address.
**********************
* SETOUT
* Set port dir for o/p
*
SETOUT ldx V.PORT,u load port address
clr 1,x clear control reg
lda #PBOUTP set port for o/p
sta ,x
lda #PBCREG reload ctrl reg
sta 1,x
clrb
rts
**********************
* SETIN ldx V.PORT,u
* Set port dir for i/p
*
SETIN ldx V.PORT,u
clr 1,x
lda #PBINP
sta ,x
lda #PBCREG
sta 1,x
clrb
rts
The first of the device driver procedures INIT, just configures the port as
input and returns. All routines should return with B=0 and the carry bit clear
unless you wish to report an error, in which case the B register should
contain the OS9 error code. Note, that rts is used to return from a device
driver routine, not os9 f$exit as per normal OS9 programs.
**********************
* INIT
* Entry: U = Static storage
* Setup the PIA
*
INIT bsr setin
clrb
rts
The READ routine is required to fetch a data item from the IO port. For SCF
type devices, this is 1 character and should be returned in the A register.
**********************
* READ
* Entry: U = Static Storage
* Y = Path Descriptor
* Exit: A = Character read
*
* Read a byte from Port B
*
READ ldx V.PORT,u load port address
readlp tst 1,x test for data ready
bmi readbyte
pshs x sleep for a bit if not
ldx #10
os9 f$sleep
puls x
bra readlp
readbyte lda ,x read byte
sta ,x issue strobe
clrb
rts
The READ routine makes use of another OS9 system call - sleep. On entry, X
should contain the number of clock cycles to sleep for (clock cycle = 1/50
second on the Dragon). This is not strictly required, however when an IO
request is performed OS9 prevents any other process from running until it
completes or executes a sleep request. For example, if you had a process
running as a background task using this device and the device stalled (ie. it
never became ready) your machine would lockup. By sleeping for a short while
between checks you ensure this cannot happen.
The WRITE routine acts in a similar way to READ, except the character passed
in the A register should be written to the port:
**********************
* WRITE
* Entry: U = Static storage
* Y = Path Descriptor
* A = Char to write
*
* Write a byte to Port B
*
WRITE ldx V.PORT,U load port address
sta ,x write byte
writlp tst 1,x wait for ack
bmi wrt
pshs x
ldx #1
os9 f$sleep
puls x
bra writlp
wrt lda ,x clear control reg
ok clrb
rts
The next two routines demonstrate device driver usage of the GetStat and
SetStat calls and also the path descriptor.
There are a few predefined GetStat calls under OS9 which nearly all SCF device
drivers will have to deal with. These are Test for Data Ready and Test for End
Of File. The predefined GetStat codes are defined in the OS9Defs file but be
warned: a lot are device dependent and will not be implemented. Generally,
only the ones listed above are available on all SCF devices.
GetStat calls are made by the caller by issuing an OS9 i$getstt call with the
A register containing the path number and the B register containing the
GetStat code - the other register contents depend on the GetStat call being
made. This example program loops until data is available from the keyboard:
chklp clra pathnum of stdin
ldb #SS.Ready getstat code
os9 i$getstt issue call
bcc okay if c bit clear data is rdy
cmpb #E$NRDY otherwise check the code
bne error returned was not ready
bra chklp
In order to write a routine to process a GetStat call, you need to access the
contents of the registers when the call was made - in this case the B register
to obtain the getstat code. Within the common part of the path descriptor is a
pointer to a data area which contains all the register contents when the call
was made. Labels set up in the os9defs file make accessing this information
easy.
The following GetStat routine processes 3 codes. It first retrieves the
register pointer from the path descriptor then loads what was in the B
register into the A register using the R$B label to find the location. The 3
getstat codes are SS.Ready - test for data ready which is performed by using
the PIA to determine if a data byte is waiting to be processed or not, SS.EOF
- end of file which for this device has no meaning and therefore always
returns with no error ie. not end of file. The final code is a user defined
one I created specific for this device named SS.VSupv. When called with this
code it forces the port to change direction to input using the 'setin'
procedure defined earlier. All other codes return an error.
**********************
* GETSTA
* U = Static Storage
* Y = Path Descriptor
*
GETSTA ldx PD.Rgs,y X=pointer to registers
lda R$B,x A=contents of B reg.
cmpa #SS.Ready check for ready code
bne gsta1 if not, check next
ldx V.Port,u use the PIA to determine
tst 1,x if data is available
bmi ok
ldb #E$NotRdy
coma sets the carry flag
rts
gsta1 cmpa #SS.EOF check for eof code
beq ok which always returns ok
cmpa #SS.VSupv check for a special code
beq setin which changes port dir
comb otherwise return
ldb #E$UnkSVC unknown code error
rts
The SetSta call works in an identical way to GetStat, and generally SCF
devices do not use SetStat calls so you can just return the E$UnkSVC error
immediatly. For our example driver, SetStat supports the new SS.VSupv code
which when received will force the port direction to be output using the
'setout' procedure:
**********************
* PUTSTA
* Supv request sets port to o/p
*
PUTSTA ldx PD.Rgs,y X=register stack
ldb R$B,x B=contents of reg B
cmpb #SS.VSupv check for recognized code
beq setout and process it
comb otherwise return error.
ldb #E$UnkSVC
rts
Finally, the TERM routine. Since the driver has not allocated any extra memory
or anything it can just return okay:
**********************
* TERM
* Terminate Driver
*
TERM clrb
rts
emod
PEND equ *
Thats just about it.
Before proceeding to an RBF device driver there is one more thing required to
complete the new SCF device - a device descriptor.
The device descriptor is just composed of a set of data items which define the
device you are creating. In particular, it is the name you refer to when you
want to access the device - so our example is called P1 so in order to access
it you just refer to this device ie.
list startup >/p1
copies the startup file to device /p1. The descriptor also contains information
to identify it's device driver, file manager and other unique information. It
also contains the port address (which is copied to V.Port so your driver can
refer to it) and in the case of SCF devices contains a whole list of attributes
referring to the capabilities of the device.
Here is the PIA device descriptor to go with the PIA21 device driver:
****************************************
* PIA Descriptor module
*
* Source by J.Bird (C) 1992
*
ifp1
use /h0/defs/os9defs
endc
nam P1
ttl PIA Device Descriptor
mod PEND,PNAM,DEVIC+OBJCT,REENT+1,PMGR,PDRV
fcb READ.+WRITE.+SHARE.
fcb $FF IOBlock (unused)
fdb $FF32 hardware address
fcb PNAM-*-1 option byte count
fcb $0 SCF device
fcb $0 Case (upper & lower)
fcb $1 Erase on backspace
fcb $0 delete (BSE over line)
fcb $0 echo off
fcb $1 lf on
fcb $0 eol null count
fcb $0 no pause
fcb 24 lines per page
fcb $8 backspace
fcb $18 delete line char
fcb $0D end of record
fcb $1b eof
fcb $04 reprint line char
fcb $01 duplicate last line char
fcb $17 pause char
fcb $03 interrupt char
fcb $05 quit char
fcb $08 backspace echo char
fcb $07 bell
fcb $00 n/a
fcb $00 n/a
fdb pnam offset to name
fdb $0000 offset to status routine
pnam fcs "P1"
pmgr fcs "SCF"
pdrv fcs "PIA21"
emod
pend equ *
end
Once again it's format is similiar to any OS9 module, with a program type of
DEVIC indicating device descriptor. The module just consists of fcb data
stataments. Generally, this may well suffice for any SCF module you care to
create. Some key features to note however are:
fcb READ.+WRITE.+SHARE.
indicates the device can be read, written and also multiple processes can
access it at any one time. Failing to specify some of these will cause the
system to return 'device not capable of operation' type errors.
fdb $FF32 hardware address
The physical hardware port address. This indicates our PIA sits at $FF32 in
memory - copied to V.Port in the device driver.
fcb $0 echo off
This is an important one. If set to non-zero when the SCF file manager reads a
byte it will automatically send it back out using the write routine. In the
case of our 6821 driver which only operates in one direction at a time this is
not a good idea.
pnam fcs "P1"
pmgr fcs "SCF"
pdrv fcs "PIA21"
Finally, these give the device descriptor name (pnam), its associated file
manager (pmgr) and device driver (pdrv) and is the only way the system has of
working this out.
As an additional note, the device driver INIT also sets the Y register to point
to the device descriptor data should you need to access it. Most of the
parameters are also copied into the path descriptor option section where you
can access them if required. Remember, any modifications you make to the path
descriptor locations will only be valid for the duration the device or file is
'open'.
Once you have your new driver and descriptor you just load them into memory and
then any OS9 program can use them:
load pia21
load p1
list startup >/p
RBF Device Driver
RBF drivers tend to be a little more complex than SCF ones but not drastically
so. In addition, practically everything covered in the SCF device also applies
to RBF device covered here. They also tend to open up your system a lot more -
essentially you are getting another disk drive out of it. The example I am
using here is based around the
RAM disc design
I wrote a couple of years ago in Up2Date.
The RAM disc design used once again an MC6821 PIA, which utilised one side to
select a RAM function to be performed and the other side as the data bus. The
design was specifically built to make it easy to interface into an OS9 system.
For RBF devices, the read and write functions are required to transfer a 256
byte sector from a given 24 bit logical sector number (LSN). LSNs give a means
of addressing disks without having to worry about tracks/sectors per track.
They are numbered from 0 upwards starting (on a 'real' disk anyway) as LSN
0=track 0, sector 1 etc. On a standard Dragon disk (40T/SS) LSN 0=Track 0,
sector 1, LSN 1=Track 0, sector 2, LSN 18=Track 1, sector 1 etc.
The PIA design allowed for the following operations:
1. Write MSB of LSN. Write the top 16 bits of the LSN to the RAM drive (the top
8 bits are discarded as you would need a phenominal amount of RAM to need these
bits).
2. Write LSN of LSN. Write the botton 8 bits of the LSN to the RAM drive.
3. Read a sector. Transfer the sector byte by byte from the RAM drive hardware
to a buffer.
4. Write a sector. Transfer a sector byte by byte from a buffer to the RAM
drive hardware.
So, there is really minimal processing involved to utilise the RAM drives. Most
floppy, or hard disk drivers would normally require some LSN to track/sector
conversion.
All RBF device drivers differ subtly from SCF drivers in that there are
certain mandatory things you must perform within them in order for them to
function correctly. I will attempt to explain these throughout the driver
example.
An RBF driver starts off like all OS9 modules, with the standard header
format. Here you see I am up to revision 6 of the PIA RAM disk driver:
*********************************************
* Rdisk
* A driver for a Ram disk!
* By G.Twist (c) 1986
* modified by Bernd H. Neuner 1987
* Version for a totally different Ram disk!
*
* By J.B. & O.B. (C) 1991,1992
*
nam Rdisk
ttl A Device Driver for a RAM Disk
ifp1
use /d0/defs/os9defs
use /d0/defs/iodefs
use /d0/defs/rbfdefs
endc
***********************
* Edition History
* # date Comments
* -- -------- ----------------------------------------------
* 1 86/12/17 Driver first developed GDT
* 2 86/12/18 work to fix minor access bugs GDT
* 3 30.11.87 bug in COPY routine fixed. BHN (no more error 214 now.)
* 4 31.12.91 Test version for main RAM JB/OB
* 5 08.01.92 Driver for 128K PIA RAM JB/OB
* 6 18.09.92 Up-issue to support up to 512K JB
Revision equ 6
NumDrvs set 1 Number of drives
* pia control comands
pia.ddr equ %00111000
pia.off equ %00111100
pia.act equ %00101100
ext.msr equ %00000001
ext.lsr equ %00001000
ext.read equ %00000010
ext.writ equ %00000110
output equ %11111111
outb equ %00001111
pia.iora equ 0
pia.cnra equ 1
pia.iorb equ 2
pia.cnrb equ 3
org Drvbeg
rmb NumDrvs*DrvMem
LSNZERO rmb 1
RAMSTA equ .
mod RAMEND,RAMNAM,Drivr+Objct,Reent+Revision,RAMENT,RAMSTA
RAMNAM fcs /Rdisk/
RAMENT lbra INIT
lbra READ
lbra WRITE
lbra GETSTA
lbra PUTSTA
lbra TERM
Along with the equates needed for the driver, the RBF driver static storage is
being utilised (note the V.SCF equate replaced with DRVBEG). The first
mandatory thing about an RBF driver is that you must allocate a drive table
for EACH drive you plan the driver to deal with. The drive table will hold
information particular to the drive (such as number of sectors/track etc.) and
must be the first block of data in your static storage. You should reserve
DRVMEM bytes (DRVMEM is defined in the rbfdefs file) multiplied by the number
of drives your driver can handle (in this case NumDrvs is defined as 1). So
the DDISK OS9 driver reserves 4 times this amount to handle the 4 possible
floppy disks on a Dragon system. The only data item declared for our use is
the LSNZERO byte.
The INIT routine of the driver also requires some mandatory code required by
all RBF drivers:
1. Initialise the V.NDRV variable of the RBF static storage to the number of
drives the driver will deal with.
2. Initialise the DD.TOT and V.Trak variables held within the drive tables of
the static storage of EACH drive to a non-zero value.
In this driver, although it is only targetted for 1 drive, there is a loop
construct present for dealing with multiple drives should this ever be a
requirement. In addition, our INIT routine also sets up the PIA and
initialises our own data item.
*****************************
* INIT
* Set up the ramdisk
INIT lda #NumDrvs Set no drives to 1
sta V.NDRV,U
clr LSNZERO,U Initialise our data
lda #$FF non-zero value
leax DrvBeg,U X=Start of drive table
initdrv
sta DD.Tot,X write in non-zero values
sta V.Trak,X
leax DrvMem,x
deca loop through drives
bne initdrv
* set up pia
ldx V.PORT,U
lda #pia.off
sta pia.cnra,x select a side off
lda #pia.ddr
sta pia.cnrb,x select b side ddr
lda #outb
sta pia.iorb,x set ls 4 bits to outputs
lda #pia.off
sta pia.cnrb,x select b side io reg
clr pia.iorb,x
clrb
INITXIT rts
The READ routine is required to transfer an entire disk sector to a buffer.
The 24 bit LSN number to read is held in the B and X registers as follows:
bits#
23 ... 15 ... 7 ... 0
| B | X |
The VALID procedure actually performs the sector validation and loading of
this data into the PIAs. The sector is then transfered into the sector buffer
pointed to by the PD.BUF location in the path descriptor.
The READ routine (surprisingly enough) also has to perform some mandatory
operations - in particular when LSN 0 is read. This contains disk ID
information and whenever a request is made to read this sector, the driver is
required to copy DD.SIZ bytes into the drive table for the required drive:
*****************************
* READ
* read a sector from disk
* Entry: U = Static Storage
* Y = Path Descriptor
* B = MSB of LSN
* X = LSB's of LSN
* Exit: 256 byte sector in PD.BUF buffer
*
READ clr LSNZERO,U init LSNZERO flag
bsr VALID validate the LSN supplied
bcs READ99 raise error if invalid
pshs Y preserve pointer to PD
ldy PD.BUF,Y Y=start of sector buffer
ldx V.PORT,U X=PIA port address
lda #pia.ddr program PIA for transfer
sta pia.cnra,x
clr pia.iora,x
lda #pia.act
sta pia.cnra,x
lda #ext.read
sta pia.iorb,x
leax pia.iora,x
clrb
READS10 lda ,x transfer sector from PIA
sta ,y+
decb
bne READS10
leax V.Port,u reset PIA
clr pia.iorb,x
lda #pia.off
sta pia.cnra,x
puls Y
*mandatory RBF code - copy LSN 0
tst LSNZERO,U VALID routine will set this
beq READ90 if LSN 0 specified
lda PD.DRV,Y extract drive num
ldb #DRVMEM size of drive table
mul
leax DRVBEG,U start of drive tables
leax D,X add on drive offset
ldy PD.BUF,Y copy DD.Siz bytes
ldb #DD.SIZ-1 into drive table
COPLSN0 lda B,Y
sta B,X
decb
bpl COPLSN0
READ90 clrb
READ99 rts
The sector write routine works in a similar manner: the LSN is supplied in the
same format, and this time the sector held in PD.BUF must be transferred to
the RAM disk PIAs. No special processing is required for LSN 0.
*****************************
* WRITE
* Write a sector to disk
* Entry: U = Static Storage
* Y = Path Descriptor
* B = MSB of LSN
* X = LSB's of LSN
* PD.Buf = Sector to write
*
WRITE bsr VALID validate sector
bcs WRIT99
WRITS pshs Y save PD
ldy PD.BUF,Y Y=sector buffer
ldx V.PORT,U X=port address
lda #pia.ddr program PIA
sta pia.cnra,x for write txfr
lda #output
sta pia.iora,x
lda #pia.act
sta pia.cnra,x
lda #ext.writ
sta pia.iorb,x
leax pia.iora,x
clrb
WRITS10 lda ,y+ transfer sector
sta ,x to PIA
lda ,x
decb
bne WRITS10
ldx V.PORT,U restore PIA
clr pia.iorb,x
lda #pia.off
sta pia.cnra,x
puls Y
clrb
WRIT99 rts
The VALID subroutine is shown below. This serves to program the PIA with the
LSN supplied - note the top part of the LSN held in the B register is
discarded for this driver, only the X part is used.
*****************************
* VALID
* validate a sector
* and set up external registers
* to reqired page in ram
*
VALID
cmpx #$0000 check for LSN 0
bne NOTLSN0 set flag appropriatly
inc LSNZERO,U
NOTLSN0 pshs y
ldy V.PORT,U
lda #pia.ddr select direction reg
sta pia.cnra,y
lda #output set bits to output
sta pia.iora,y
lda #pia.act enable ca2 strobe
sta pia.cnra,y
lda #ext.msr select ms sector reg
sta pia.iorb,y
tfr x,d
sta pia.iora,y write ms value
lda pia.iora,y do the read (strobe)
lda #ext.lsr select ls sector reg
sta pia.iorb,y
stb pia.iora,y write ls value
ldb pia.iora,y do the read (strobe)
clr pia.iorb,y select nothing
puls y note a side still set for output
clrb
rts
On the GETSTA and SETSTA side of things, there are no 'mandatory' getsta codes
you need to deal with and two SETSTA codes: SS.RST (restore head to track 0)
and SS.WRT (write track). Restore head to track 0 serves no useful purpose on
a RAM disc and can just return okay. The SS.WRT call is used during format
operations to actually format the track - again for a RAM drive this will not
be required and can just return okay. All others return with an error:
**************************
* GETSTA
* get device status
*
GETSTA
Unknown comb
ldb #E$UnkSVC
rts
**************************
* PUTSTA
* Set device Status
*
PUTSTA cmpb #SS.Reset
beq PUTSTA90
cmpb #SS.WTrk
bne Unknown
PUTSTA90 clrb
rts
Finally, the TERM routine simply exits cleanly:
*****************************
* TERM
* terminate Driver
*
TERM clrb
rts
emod
RAMEND equ *
All that remains to cover is the RBF device descriptor, which is actually a
lot simpler than the SCF one:
******************************************
* RamDisk Discriptor module
*
*
ifp1
use /d0/defs/os9defs
endc
nam R0
ttl Drive Discriptor module
mod DEnd,DNam,DEVIC+OBJCT,REENT+1,DMgr,DDrv
fcb DIR.+SHARE.+PREAD.+PWRIT.+UPDAT.+EXEC.+PEXEC.
fcb $FF IOBlock (unused)
fdb $FF34 hardware address
fcb DNam-*-1 option byte count
fcb $1 Rbf device
fcb 0 Drive number
fcb 03 6ms Step rate
fcb $80 Standard OS9 Winchester drive
fcb 0 Single density
fdb 4 number of tracks
fcb 1 number of sides
fcb 1 dont verify any writes
fdb 256 sectors per track
fdb 256 sectors on track 0, side 0
fcb 1 sector interleave factor
fcb 1 sector allocation size
DNam fcs "R0"
DMgr fcs "RBF"
DDrv fcs "RDisk"
emod
DEnd equ *
end
This is the device descriptor named R0 for our RAM drive. It shares similar
properties to the SCF one, firstly the attributes byte indicating full access
(READ.+WRITE. etc. ) including directory access. There is also the familiar
hardware address and at the end the device descriptor name, file manager name
and driver name. Most of the descriptor is composed of the drive attributes:
drive number, total tracks, sectors per track etc. Since this is a RAM drive
all I have done is ensured the total sectors on the media is equal to the
amount of RAM I have available - in this case 256K.
Once assembled, you can simply load and access the RAM drive like any other
disk on your system:
load rdisk
load r0
chd /r0
copy /d0/startup /r0/startup #32k
etc.
That just about wraps it up. A couple of other points worth noting: it's
normally worth either checking out or basing your driver on someone else's as
a good starting point, there is also a fair deal of information around
particularly from the CoCo side of things. And something I'd always recommend:
as you have seen is nearly always easier to debug stuff under normal DragonDOS
so I'd suggest at least prototyping your code under this environment to see if
at least the logic works how you think it should. That way, when you port it
to OS9 all you have to worry about is the quirks of OS9 rather than your own
driver logic.