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:


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:


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
---- ---- -- -- -- -- ---------
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
50 JSR $90E5
60 RTS

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

  use /d0/defs/os9defs

*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
error os9 f$exit


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:


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 Progamming 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:

 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

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

 use /h0/defs/os9defs
 use /h0/defs/iodefs
 use /h0/defs/scfdefs

 org V.SCF
PST equ .

type equ DRIVR+OBJCT
revs equ REENT+1
 mod PEND,PNAM,type,revs,PENT,PST

PNAM fcs /DEVx/

 lbra READ
 lbra WRITE
 lbra GETSTA
 lbra PUTSTA
 lbra TERM

INIT   .
READ   .
TERM   .

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

 use /h0/defs/os9defs
 use /h0/defs/iodefs
 use /h0/defs/scfdefs

* 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
PNAM fcs /PIA21/

 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.

*   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

* SETIN ldx V.PORT,u
*   Set port dir for i/p
 clr 1,x
 lda #PBINP
 sta ,x
 lda #PBCREG
 sta 1,x

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.

* Entry: U = Static storage
*   Setup the PIA
INIT bsr setin

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.

* 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

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:

* 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

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.

*   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
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

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:

*   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

Finally, the TERM routine. Since the driver has not allocated any extra memory or anything it can just return okay:

*   Terminate Driver
TERM clrb

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
 use /h0/defs/os9defs

 nam P1
 ttl PIA Device Descriptor


 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"
pend equ *

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:


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

Complete source code listing

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

 use /d0/defs/os9defs
 use /d0/defs/iodefs
 use /d0/defs/rbfdefs

* 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 equ %00111100
pia.act equ %00101100
ext.msr equ %00000001
ext.lsr equ %00001000 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/

 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.

*  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
 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
 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
 sta pia.cnrb,x   select b side io reg
 clr pia.iorb,x

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:

  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 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
 sta pia.iorb,x
 leax pia.iora,x
READS10 lda ,x          transfer sector from PIA
 sta ,y+
 bne READS10
 leax V.Port,u          reset PIA
 clr pia.iorb,x
 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
 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
 sta B,X
 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 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
WRITS10 lda ,y+          transfer sector
 sta ,x                  to PIA
 lda ,x
 bne WRITS10
 ldx V.PORT,U            restore PIA
 clr pia.iorb,x
 sta pia.cnra,x
 puls Y
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.

*  validate a sector
*  and set up external registers
*  to reqired page in ram
 cmpx #$0000     check for LSN 0
 bne NOTLSN0     set flag appropriatly
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

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:

*  get device status
Unknown comb
 ldb #E$UnkSVC

*  Set device Status
PUTSTA cmpb #SS.Reset
 beq PUTSTA90
 cmpb #SS.WTrk
 bne Unknown

PUTSTA90 clrb

Finally, the TERM routine simply exits cleanly:

*  terminate Driver
TERM clrb

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
 use /d0/defs/os9defs

 nam R0
 ttl Drive Discriptor module


 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"
DEnd equ *

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


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.

Complete source code listing