Skip to content

OS 9 Software

Boisy Gene Pitre, PhD edited this page May 13, 2026 · 2 revisions

Author: Jon Bird

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

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).

  1. Write LSN of LSN. Write the botton 8 bits of the LSN to the RAM drive.

  2. Read a sector. Transfer the sector byte by byte from the RAM drive hardware to a buffer.

  3. 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.

Clone this wiki locally