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.
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:
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:
The DIR command is not recognized by the Shell, therefore it automatically attempts to load and run it from elsewhere. The Shell itself contains relativly few in-built commands (unlike the BASIC interpretor) and so for the majority of things will be forcing the execution of an external program.
One of the utility programs shipped with Dragon OS9 is MDIR which simply provides a list of things in memory, or a very simplified OS9 memory map. When I run it on my system I get something like the following:
Module Directory at 10:47:24 ADDR SIZE TY RV AT UC NAME ---- ---- -- -- -- -- --------- F05E 7E7 C1 1 r OS9 F853 4FC C1 1 r 1 OS9p2 FD4F 2E C0 1 r 1 Init FD7D 182 C1 1 r 1 BooT BF00 122 C1 4 r 1 SYSGO C022 101 C1 1 r 1 Clock C123 6E7 C1 1 r 1 IOMan C80A CF5 D1 1 r 1 Rbf D4FF 40E D1 1 r 2 Scf D90D 1FF D1 1 r PipeMan DB0C 5E2 E1 7 r Ddisk E0EE 3AF E1 6 r 2 Kbvdio E49D 8D E1 1 r Printer E52A 28 E1 1 r Piper E552 2E F1 1 r D0 E580 2E F1 2 r D1 E5AE 3C F1 1 r 2 Term E5EA 3A F1 1 r P E624 26 F1 1 r Pipe E64A 4FA 11 1 r 2 Shell EB44 121 E1 3 r 1 Rdisk EC65 2E F1 1 r 1 R0 EC93 160 E1 1 r PC_Disk EDF3 30 F1 1 r H0 5A00 1A6 11 1 r 1 Mdir
The majority of these modules are the drivers, managers and general routines that make up the 'core' of the OS9 system. In fact, the only programs as such are the Shell, and the MDIR program itself. One of the things to note, is that if you run MDIR on your OS9 system, it would almost certainly look different: not only would you probably have different drivers, they would almost certainly appear in different areas of memory. This is one of the key differences between OS9 and normal Dragon BASIC mode, that any program written for OS9 does not know where it will be in memory and therefore has to be written to be run from any location in memory. One of the side effects of this is that code modules in OS9 will probably tend to execute slower that a DragonDOS counterpart. Part of this is also due to the fact that within the Dragon BASIC/DOS ROM routines always lie at fixed addresses, and can be called quickly. OS9 programs have to perform more complex indexing operations to find routines. Therefore, OS9 always seems considerably more slower and therefore more painful to work with than DragonDOS.
To see this, and some of the pros and cons of writing programs for each system, here is a couple of 'hello world' routines for each system. Firstly, the Dragon assembler version - this assumes DASM to be the assembler:
10 EXEC &H6C00 20 ALL 30 @MSG FCC 0,"HELLO WORLD",0 40 @START LDX #@MSG 50 JSR $90E5 60 RTS 70 END @START 80 EXEC
It is a fairly simple piece of code, and also fairly easy to write, assemble and get running. Firstly, DASM is loaded from disk, you write the code much like a BASIC program using all the normal BASIC editor functions then just run it. The program itself makes use of a standard ROM routine, which prints a string pointed to by the X register. The 'END' statement at line 70 puts the starting address into the default EXEC location, which is then called by line 80.
Onto the OS9 version then:
nam WORLD ttl Hello World program ifp1 use /d0/defs/os9defs endc *Data area org 0 stack rmb 200 datsiz equ . *Module header type equ PRGRM+OBJCT revs equ REENT+1 mod length,name,type,revs,start,datsiz name fcs /WORLD/ msg fcc /Hello World/ fcb 13 *main code start leax msg,pcr ldy #12 lda #1 os9 i$writln bcs error clrb error os9 f$exit emod length equ *
One of the first problems is actually getting an environment to write this in. Dragon OS9 comes with an edit command, which in all honestly is complete rubbish and I'd suggest you use Stylo or another editor. I use a program called 'ed' which is based on the Unix 'vi' editor. Once you've written it, saved it to disk, you can assemble it using the OS9 assembler:
asm world #20k
Assumes you've saved the file as world. Assuming all goes well, you will end up with a file called 'world' in your default execution directory (typically /d0/cmds). However, it is the norm for OS9 (I have found anyway) that things are not often that straightforward. Firstly, you may find you don't actually have a copy of the 'asm' program, and even if you do the program you have just written requires an additional file (called up by the 'use /d0/defs/os9defs' statement) which may or may not be on your disk. Either way, you will probably end up searching through a variety of disks in order to get the files required to the right place. Once you have, and a great deal of disk chugging later with any luck, simply typing 'world' at the OS9 shell prompt, should once again after a bit of disk chugging display the message 'hello world' on your screen.
So how does it all work, and is it all worth it. Firstly, the NAM and TTL directives just tell the assembler the name and title of your program and are not strictly required. Next, there is an assembler directive to include the file /d0/defs/os9defs but only on pass one (the IFP1 condition) of the assembly. This file contains a list of definitions about OS9 and is almost always required for any OS9 module. Whilst you can get away without it, for general neatness and code readability it's a good idea to use it. For example, in our simple program, the words PRGRM, OBJCT, i$write and f$exit are all definitions from os9defs.
Next the program's data area is defined, which as a minimum should define at least 200 bytes for the stack (I'll cover data areas later). Following this, information for the programs module header is defined. In OS9, each program unit or module must conform to a specific format in order for OS9 to recognize it and successfully execute it. In particular, it contains information about the execution address, how much data size is required and also a checksum of the whole module. All this minimizes the risk of trying to run corrupted or invalid code. The 'mod' assembler directive tells the assembler to form this header using the information provided. This requires the overall length of the module then a pointer to the name of the module. Next are two equates defined as TYPE and REVS. The TYPE equate is a single byte defining the module type and language and is preset to a normal program module (PRGRM) and 6809-object code (OBJCT). The REVS equate is a single byte defining the module attributes and revision level. It has an attribute of sharable (REENT) and a revision level of 1. The final two parameters are the start address and overall module data size.
Following the module header is the module name. The FCS directive is much like the FCC directive except the top bit of the last character is the string is set. This is often used by OS9 to indicate the end of a string. Finally, the main code for the program occurs.
The code make use of the OS9 system call i$writln. In order to make a system call, the assembler directive 'os9' is used followed by the call name. All the valid OS9 call names are defined in the os9defs file. OS9 system calls are made through one of the software interrupts, in this case SWI2 followed by a unique code identifying the call number. Therefore, the assembler directive 'os9 i$writln' translates into 'SWI2 $8C' where $8C is the i$writln code.
This call is much like the ROM call at $90E5, where the X register points to the string to display. In addition, however the Y register should also contain the maximum number of characters to display and the A register should contain the path number. A path number of 1 refers to the standard output - the display. The call will then display the required number of characters, or up to the first carriage return - whichever occurs sooner. On completion of the call, the carry bit is set if an error has occured with the error number in the B register. Finally, the routine is terminated with a call to the F$EXIT routine not an RTS instruction.
Once the code is complete, the EMOD statement indicates that this is the end of the module, and generates the module's checksum and then the length label is calculated.
This program also gives an example writing position independent code ie. code that can be executed anywhere in memory. Note how the X register in the os9 example is set to point to the string compared to the DragonDOS one:
DDOS: LDX #@MSG - absolute address of MSG OS9: leax msg,pcr - relative address of MSG
The OS9 variant sets X to point to the MSG label relative to the current program counter ie. whereever you are in memory. The DragonDOS one specifies the absolute address in memory. This means that with the DragonDOS code you are forced to have your program at this location in order for it to work. Of course, you could replace your DragonDOS one with the same instruction as the OS9 one and make it position independent, but with OS9 you must use this format or it will not work.
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:
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:
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.
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:
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]
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.
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
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:
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.
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:
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 drivers tend to be a little more complex than SCF ones but not drastically so. In addition, practically everything covered in the SCF device also applies to RBF device covered here. They also tend to open up your system a lot more - essentially you are getting another disk drive out of it. The example I am using here is based around the RAM disc design I wrote a couple of years ago in Up2Date.
The RAM disc design used once again an MC6821 PIA, which utilised one side to select a RAM function to be performed and the other side as the data bus. The design was specifically built to make it easy to interface into an OS9 system.
For RBF devices, the read and write functions are required to transfer a 256 byte sector from a given 24 bit logical sector number (LSN). LSNs give a means of addressing disks without having to worry about tracks/sectors per track. They are numbered from 0 upwards starting (on a 'real' disk anyway) as LSN 0=track 0, sector 1 etc. On a standard Dragon disk (40T/SS) LSN 0=Track 0, sector 1, LSN 1=Track 0, sector 2, LSN 18=Track 1, sector 1 etc.
The PIA design allowed for the following operations:
1. Write MSB of LSN. Write the top 16 bits of the LSN to the RAM drive (the top 8 bits are discarded as you would need a phenominal amount of RAM to need these bits).
2. Write LSN of LSN. Write the botton 8 bits of the LSN to the RAM drive.
3. Read a sector. Transfer the sector byte by byte from the RAM drive hardware to a buffer.
4. Write a sector. Transfer a sector byte by byte from a buffer to the RAM drive hardware.
So, there is really minimal processing involved to utilise the RAM drives. Most floppy, or hard disk drivers would normally require some LSN to track/sector conversion.
All RBF device drivers differ subtly from SCF drivers in that there are certain mandatory things you must perform within them in order for them to function correctly. I will attempt to explain these throughout the driver example.
An RBF driver starts off like all OS9 modules, with the standard header format. Here you see I am up to revision 6 of the PIA RAM disk driver:
********************************************* * Rdisk * A driver for a Ram disk! * By G.Twist (c) 1986 * modified by Bernd H. Neuner 1987 * Version for a totally different Ram disk! * * By J.B. & O.B. (C) 1991,1992 * nam Rdisk ttl A Device Driver for a RAM Disk ifp1 use /d0/defs/os9defs use /d0/defs/iodefs use /d0/defs/rbfdefs endc *********************** * Edition History * # date Comments * -- -------- ---------------------------------------------- * 1 86/12/17 Driver first developed GDT * 2 86/12/18 work to fix minor access bugs GDT * 3 30.11.87 bug in COPY routine fixed. BHN (no more error 214 now.) * 4 31.12.91 Test version for main RAM JB/OB * 5 08.01.92 Driver for 128K PIA RAM JB/OB * 6 18.09.92 Up-issue to support up to 512K JB Revision equ 6 NumDrvs set 1 Number of drives * pia control comands pia.ddr equ %00111000 pia.off equ %00111100 pia.act equ %00101100 ext.msr equ %00000001 ext.lsr equ %00001000 ext.read equ %00000010 ext.writ equ %00000110 output equ %11111111 outb equ %00001111 pia.iora equ 0 pia.cnra equ 1 pia.iorb equ 2 pia.cnrb equ 3 org Drvbeg rmb NumDrvs*DrvMem LSNZERO rmb 1 RAMSTA equ . mod RAMEND,RAMNAM,Drivr+Objct,Reent+Revision,RAMENT,RAMSTA RAMNAM fcs /Rdisk/ RAMENT lbra INIT lbra READ lbra WRITE lbra GETSTA lbra PUTSTA lbra TERM
Along with the equates needed for the driver, the RBF driver static storage is being utilised (note the V.SCF equate replaced with DRVBEG). The first mandatory thing about an RBF driver is that you must allocate a drive table for EACH drive you plan the driver to deal with. The drive table will hold information particular to the drive (such as number of sectors/track etc.) and must be the first block of data in your static storage. You should reserve DRVMEM bytes (DRVMEM is defined in the rbfdefs file) multiplied by the number of drives your driver can handle (in this case NumDrvs is defined as 1). So the DDISK OS9 driver reserves 4 times this amount to handle the 4 possible floppy disks on a Dragon system. The only data item declared for our use is the LSNZERO byte.
The INIT routine of the driver also requires some mandatory code required by all RBF drivers:
1. Initialise the V.NDRV variable of the RBF static storage to the number of drives the driver will deal with.
2. Initialise the DD.TOT and V.Trak variables held within the drive tables of the static storage of EACH drive to a non-zero value.
In this driver, although it is only targetted for 1 drive, there is a loop construct present for dealing with multiple drives should this ever be a requirement. In addition, our INIT routine also sets up the PIA and initialises our own data item.
***************************** * INIT * Set up the ramdisk INIT lda #NumDrvs Set no drives to 1 sta V.NDRV,U clr LSNZERO,U Initialise our data lda #$FF non-zero value leax DrvBeg,U X=Start of drive table initdrv sta DD.Tot,X write in non-zero values sta V.Trak,X leax DrvMem,x deca loop through drives bne initdrv * set up pia ldx V.PORT,U lda #pia.off sta pia.cnra,x select a side off lda #pia.ddr sta pia.cnrb,x select b side ddr lda #outb sta pia.iorb,x set ls 4 bits to outputs lda #pia.off sta pia.cnrb,x select b side io reg clr pia.iorb,x clrb INITXIT rts
The READ routine is required to transfer an entire disk sector to a buffer. The 24 bit LSN number to read is held in the B and X registers as follows:
bits# 23 ... 15 ... 7 ... 0 | B | X |
The VALID procedure actually performs the sector validation and loading of this data into the PIAs. The sector is then transfered into the sector buffer pointed to by the PD.BUF location in the path descriptor.
The READ routine (surprisingly enough) also has to perform some mandatory operations - in particular when LSN 0 is read. This contains disk ID information and whenever a request is made to read this sector, the driver is required to copy DD.SIZ bytes into the drive table for the required drive:
***************************** * READ * read a sector from disk * Entry: U = Static Storage * Y = Path Descriptor * B = MSB of LSN * X = LSB's of LSN * Exit: 256 byte sector in PD.BUF buffer * READ clr LSNZERO,U init LSNZERO flag bsr VALID validate the LSN supplied bcs READ99 raise error if invalid pshs Y preserve pointer to PD ldy PD.BUF,Y Y=start of sector buffer ldx V.PORT,U X=PIA port address lda #pia.ddr program PIA for transfer sta pia.cnra,x clr pia.iora,x lda #pia.act sta pia.cnra,x lda #ext.read sta pia.iorb,x leax pia.iora,x clrb READS10 lda ,x transfer sector from PIA sta ,y+ decb bne READS10 leax V.Port,u reset PIA clr pia.iorb,x lda #pia.off sta pia.cnra,x puls Y *mandatory RBF code - copy LSN 0 tst LSNZERO,U VALID routine will set this beq READ90 if LSN 0 specified lda PD.DRV,Y extract drive num ldb #DRVMEM size of drive table mul leax DRVBEG,U start of drive tables leax D,X add on drive offset ldy PD.BUF,Y copy DD.Siz bytes ldb #DD.SIZ-1 into drive table COPLSN0 lda B,Y sta B,X decb bpl COPLSN0 READ90 clrb READ99 rts
The sector write routine works in a similar manner: the LSN is supplied in the same format, and this time the sector held in PD.BUF must be transferred to the RAM disk PIAs. No special processing is required for LSN 0.
***************************** * WRITE * Write a sector to disk * Entry: U = Static Storage * Y = Path Descriptor * B = MSB of LSN * X = LSB's of LSN * PD.Buf = Sector to write * WRITE bsr VALID validate sector bcs WRIT99 WRITS pshs Y save PD ldy PD.BUF,Y Y=sector buffer ldx V.PORT,U X=port address lda #pia.ddr program PIA sta pia.cnra,x for write txfr lda #output sta pia.iora,x lda #pia.act sta pia.cnra,x lda #ext.writ sta pia.iorb,x leax pia.iora,x clrb WRITS10 lda ,y+ transfer sector sta ,x to PIA lda ,x decb bne WRITS10 ldx V.PORT,U restore PIA clr pia.iorb,x lda #pia.off sta pia.cnra,x puls Y clrb WRIT99 rts
The VALID subroutine is shown below. This serves to program the PIA with the LSN supplied - note the top part of the LSN held in the B register is discarded for this driver, only the X part is used.
***************************** * VALID * validate a sector * and set up external registers * to reqired page in ram * VALID cmpx #$0000 check for LSN 0 bne NOTLSN0 set flag appropriatly inc LSNZERO,U NOTLSN0 pshs y ldy V.PORT,U lda #pia.ddr select direction reg sta pia.cnra,y lda #output set bits to output sta pia.iora,y lda #pia.act enable ca2 strobe sta pia.cnra,y lda #ext.msr select ms sector reg sta pia.iorb,y tfr x,d sta pia.iora,y write ms value lda pia.iora,y do the read (strobe) lda #ext.lsr select ls sector reg sta pia.iorb,y stb pia.iora,y write ls value ldb pia.iora,y do the read (strobe) clr pia.iorb,y select nothing puls y note a side still set for output clrb rts
On the GETSTA and SETSTA side of things, there are no 'mandatory' getsta codes you need to deal with and two SETSTA codes: SS.RST (restore head to track 0) and SS.WRT (write track). Restore head to track 0 serves no useful purpose on a RAM disc and can just return okay. The SS.WRT call is used during format operations to actually format the track - again for a RAM drive this will not be required and can just return okay. All others return with an error:
************************** * GETSTA * get device status * GETSTA Unknown comb ldb #E$UnkSVC rts ************************** * PUTSTA * Set device Status * PUTSTA cmpb #SS.Reset beq PUTSTA90 cmpb #SS.WTrk bne Unknown PUTSTA90 clrb rts
Finally, the TERM routine simply exits cleanly:
***************************** * TERM * terminate Driver * TERM clrb rts emod RAMEND equ *
All that remains to cover is the RBF device descriptor, which is actually a lot simpler than the SCF one:
****************************************** * RamDisk Discriptor module * * ifp1 use /d0/defs/os9defs endc nam R0 ttl Drive Discriptor module mod DEnd,DNam,DEVIC+OBJCT,REENT+1,DMgr,DDrv fcb DIR.+SHARE.+PREAD.+PWRIT.+UPDAT.+EXEC.+PEXEC. fcb $FF IOBlock (unused) fdb $FF34 hardware address fcb DNam-*-1 option byte count fcb $1 Rbf device fcb 0 Drive number fcb 03 6ms Step rate fcb $80 Standard OS9 Winchester drive fcb 0 Single density fdb 4 number of tracks fcb 1 number of sides fcb 1 dont verify any writes fdb 256 sectors per track fdb 256 sectors on track 0, side 0 fcb 1 sector interleave factor fcb 1 sector allocation size DNam fcs "R0" DMgr fcs "RBF" DDrv fcs "RDisk" emod DEnd equ * end
This is the device descriptor named R0 for our RAM drive. It shares similar properties to the SCF one, firstly the attributes byte indicating full access (READ.+WRITE. etc. ) including directory access. There is also the familiar hardware address and at the end the device descriptor name, file manager name and driver name. Most of the descriptor is composed of the drive attributes: drive number, total tracks, sectors per track etc. Since this is a RAM drive all I have done is ensured the total sectors on the media is equal to the amount of RAM I have available - in this case 256K.
Once assembled, you can simply load and access the RAM drive like any other disk on your system:
load rdisk load r0 chd /r0 copy /d0/startup /r0/startup #32k etc.
That just about wraps it up. A couple of other points worth noting: it's normally worth either checking out or basing your driver on someone else's as a good starting point, there is also a fair deal of information around particularly from the CoCo side of things. And something I'd always recommend: as you have seen is nearly always easier to debug stuff under normal DragonDOS so I'd suggest at least prototyping your code under this environment to see if at least the logic works how you think it should. That way, when you port it to OS9 all you have to worry about is the quirks of OS9 rather than your own driver logic.
Complete source code listing