Cromix-Plus Drivers Table of Contents 1. Introduction 1.1 Overview of the Process 1.2 General Driver Information 1.3 Suggested Languages 1.3.1 C 1.3.2 Assembler 1.4 Linking your own Driver 1.5 Debugging Mechanisms 1.6 Summary of Required Support Software 2. Driver Types, Functions and Modes 2.1 CHARACTER drivers 2.1.1 init 2.1.2 open 2.1.3 close 2.1.4 read 2.1.5 write 2.1.6 gmode 2.1.7 smode 2.2 BLOCK drivers 2.2.1 init 2.2.2 open 2.2.3 close 2.2.4 doio 2.2.5 gmode 2.2.6 smode 2.3 RAW drivers 3. Support Functions from the Cromix Plus Kernel 3.1 setlev and reslev 3.2 addint 3.3 addauto 3.4 iogetc 3.5 ioputc 3.6 putq 3.7 getq 3.8 unputq 3.9 sleep 3.10 wakeup 3.11 sgtest 3.12 sgttysig 3.13 -- deleted -- 3.14 d_printf 3.15 iodone 3.16 bfqueue 3.17 bfunqueue 3.18 lockev and unlockev 3.19 fill 3.20 zap 3.21 move 3.22 arithmetic operations 3.23 debug 3.24 lockdma and unlockdma 3.25 bu_getmode 3.26 bu_setmode 4. Header Files 4.1 BDRV.H 4.2 BLOCK.H 4.3 BMODEEQU.H 4.4 BUF.H 4.5 BUFSIZE.H 4.6 CHAR.H 4.7 DEVICE.H 4.8 ERROR.H 4.9 EVENT.H 4.10 IOBUF.H 4.11 IOLIB.H 4.12 MACROS.H 4.13 MODEEQU.H 4.14 PORT.H 4.15 QUEUE.H 4.16 RAW.H 4.17 SIGNAL.H 4.18 TMODEEQU.H 4.19 WATCHDOG.H 1. Introduction 1.1 Overview of the Process The ability to add device drivers to Cromix-Plus allows system integrators and advanced users to generate an operating system which supports special I/O devices. The following characteristics of Cromix-Plus are designed to assist user-written drivers: 1. Well-defined Driver/OS Interface 2. Complete Library of Support Functions 3. High-Level Language Support (Header files, etc) 4. Simplified Sysgen procedure. The process of writing a driver begins with an understanding of each function the operating system will require (e.g., open the device). Section 2 of this document describes these requirements for character drivers (2.1), block drivers (2.2), and raw drivers (2.3). Next, the routines which provide the required functions must be designed and written. Section 3 of this document describes a number of useful support functions which the operating system provides for the driver (e.g., interrupt control). Section 4 describes a collection of C language header files which further assist the programmer. The final tasks are as follows: compile the driver code, convert the object code into proper (.o68) format, link that code into the Cromix-Plus I/O Library, run Crogen, and begin to debug the code. Additional information about these tasks is given in the remainder of Section 1. 1.2 General Drive Information A driver is a set of functions (three, six or seven, depending on the class, raw, block, or character). Pointers to these functions are entered in a structure and the address of such a structure is the driver as far as Cromix-Plus is concerned. Cromix-Plus will call the driver only through this structure. These functions will be always called in the supervisor mode so that the drivers have full access to the system resources. The first function in each class is the initialization function. The initialization function will be called exactly once during the initial stage of the boot procedure. Initialization functions will be called with all interrupts disabled. The initialization function must not change the interrupt level or go to sleep; otherwise it might perform any action the driver finds suitable. Typical things an initialization function might do are: initialize (static) data and install interrupt routines. The remaining functions will be called as necessary, still in the supervisor mode, generally with the interrupt level set to zero. Such a function must return with the interrupt level set to the level it was called with. So, if the interrupt level is changed it must be restored before returning. The kernel will not switch processes while a driver is excuting its code, unless the driver explicitly goes to sleep (or returns from the interrupt routine). On the other side, the kernel MAY call another (or the same) function of the same driver again before such a function returns. In fact, this will happen quite often. (Of course, according to the previous section, this will happen only if the driver allows it by going to sleep). This implies that the driver code must be reentrant, at least at the beginning of each function. If a driver knows it cannot simultaneously service two requests, e.g., floppy driver, the driver must lock out other processes: there must be a flag set in the driver saying that driver is busy. If another process enters the driver it must find the driver busy. So the newcomer must go to sleep waiting for the driver to become not busy. When another process which is running in the critical part of the driver terminates critical processing, it must reset the busy flag and, of course, wake up sleepers that are sleeping because the driver was busy. A lousy implementation of a driver would be: immediately in the beginning lock out every other driver call, do all the processing without paying attention to reentrancy, unlock the driver, wake up sleepers, and return. Here is the reason why such an implementation is not desirable: while the currently active invocation of the driver is asleep, waiting for some external event, all other callers are waiting and are not allowed to enter to do something usuful in the meantime. The result is overall degradation of the system performance. The following describes in detail what the kernel knows about the drivers. All information about the drivers is contained in the following variables (see the header files in the iolib directory for typedefs) raw *raw_term; /* Pointer to raw terminal driver */ devarg rawarg; /* Its argument vector */ blk *bdevtable[bdevcnt]; /* Pointers to block device drivers */ devarg barg[bdevcnt]; /* Their argument vectors */ chd *cdevtable[cdevcnt]; /* Pointers to char device drivers */ devarg carg[cdevcnt]; /* Their argument vectors */ The file /gen/sysdef (or another, if Crogen is called accordingly) contains the data to initialize the above values. So, raw_term will be initialized to the pointer to the raw driver function table, bdevtable[] will be initialized to pointers to block device drivers, cdevtable[] will be initialized to pointers to char device drivers. The drivers in the bdevtable will be used as block device drivers for major device number 1, 2, ..., bdevcnt, and similarly for character drivers. So, major device number is determined solely by the placement in the table and the driver should NOT know where it is going to be placed (i.e., the driver should not know its own major device number at compile time). It is up to the driver to interprete the minor device number in whatever way it wishes. The devarg structures available for each driver contain the arguments that may be defined at Crogen time for a particular driver. The kernel only passes this information to the initializing function of the appropriate driver. It is up to the driver to define: are there any arguments and what they mean if there are. Further information is found in section 2. The drivers, on the other side, may know a lot about the kernel. This means that the kernel will provide a bunch of useful functions that drivers are free to use, strictly according to specifications, of course. These kernel functions either perform operations that a driver himself does not know how to do, e.g., get next character to be written, or convenient common functions that each driver does not bother to play with, e.g. integer division. A list of these functions is found in section 3. 1.3 Suggested languages 1.3.1 C language Cromix-Plus drivers are, as a rule, written in the C language. Full SVS C capabilities may be used with the following restrictions: 1. All modules must include the line #option nostack to defeat the stack checking (there is no CHKSTK function provided) 2. No floating point features in any form are provided. 3. No library functions may be used except those listed explicitly in this document. In the final stage, the drivers will be linked into Cromix. To reduce the number of globally known names, the drivers should use static functions instead of automatic functions whenever applicable, and static variables instead of global variables, whenever applicable. This is easily achieved if each driver is written as one source file. Under these conditions, the only name globally accessible should be the driver name. Note that the SVS C compiler, being a one pass compiler, requires each static function to be declared before being used. This should represent no problem as this is good practice anyhow. If for some reason a driver must be split into more than one source file, the globally known names should be carefuly restricted so as not to interfere with other names known to Cromix-Plus. For example, if you intend to write the pq driver, all global names should be of the form pq_xxxxx Note that the Cromemco CCCD C compiler pays attention only to the first eight characters of each name: though names may be longer, the compiler ignores the excess characters. The compiler distinguishes between the lower and upper case characters. However, Link68 does not which further restricts the global names. It is necessary to use the Convobj utility to convert the output from the Code generator into a form which may be linked to the rest of Cromix-Plus. The Convobj utility concatenates all static functions into a single module. If the Convobj utility is called with the -s option, the Convobj utility will concatenate all functions into a single module. However, there is a difference in the names: Names of static functions are not entry points, names of nonstatic functions remain entry points even if the functions are concatenated into a single module. The concatenation has the following effect: internal references are resolved by the Convobj utility, the iolib.o68 gets substantialy smaller (a factor of four), there is less chance to link together a half of one driver and a half of another, and so on. The following examples explain the proper use of the Convobj utility in three common cases: 1. Complete driver in a single file (example if fdctim.c) There should be no global names except the function table (= name of the driver). In particular, this means that all functions should be static. Convobj may be called with the -s option or without it, it does not matter. To verify the stated conditions, you should execute link68 -o test driver The map should list only the driver name as an entry point. The names listed as unsatisfied externals should be only those listed as being provided with the rest of Cromix-Plus. (See section 4). 2. Two drivers, say aaa.c and bbb.c, using your own common functions from the file ccc.c (example is cflop.c, uflop.c, tflop.c calling flop.c) The files aaa.c and bbb.c should be written and compiled as described above, with a proviso that names from the file ccc.c can be declared extern. The file ccc.c should again use static names except for those referenced from the files aaa.c and bbb.c (these names should be of the form ccc_xxxx and so on). This file must be converted with the -s option to ensure that it will be stuck together. As the verification, excute link -o test aaa bbb ccc Only the driver names and the names ccc_xxxx should be seen as entry points, only names listed in section 4 should remain unsatisfied externals. 3. General help functions for various drivers (example blkutl.c) The functions actualy referenced from other drivers should not be static, auxiliary local functions should be static. The Convobj utility should be called without -s option to allow a selected subset of help functions to be loaded as needed. 1.3.2 Assembler Occasionally, a part of a driver must be written in the assembly language, though this need hardly ever arises: the Cromemco CCCD C compiler can generate very good code in most applications. The best way to use assembler code is to write a function which uses no global data: all values are passed on the stack as arguments. Here are some rules: Registers D0-D2, A0-A1 are free to use Registers D3-D7, A2-A3 must be saved before use and restored before return Registers A4-A6 must must have original values if another C coded function is called, otherwise the same rule applies as for D3-D7,A2-A3 The caller (C function, presumably) will push all arguments, starting with the last one: immediately upon entry we have the following picture (SP) return address 4(SP) argument one 8(SP) argument two 12(SP) argument three ...... The pushed arguments may be destroyed, but the return address must stay where it is: after the RTS instruction the caller will simply add 4*number_of_arguments to the SP register. All arguments on the stack are always 4 byte values, and so must be the function value returned in the D0 register. If another C coded function is called from the assembler function it should be called as extern xxx jsr xxxx.L Of course, the arguments must be pushed first, the stack adjusted after the C function returns. 1.4 Linking Your Own Driver To link a new driver into Cromix-Plus requires two steps. The first step is to modify the sysdef file to inform Crogen which drivers are to be included. If the new driver is intended as a replacement for an existing one it should be placed in the old position. If this is an additional driver, a free slot has to be found in device table and the new driver inserted there. The second step is to ensure that Link68 will be able to find the new driver. To achieve this there are a number of ways to proceed. 1. Modify the iolib.mak file. Note that iolib.o68 is built gradually. First all the drivers of one class are concatenated, then the Maklib utility is called to build the library out of sublibraries. With this approach the normal crogen.cmd file will do the linking. 2. Type all .o68 files that constitute your driver to a single file (presumably, it is a single file already), then modify the crogen.cmd file to something like this: ............... %debug shift link68 -q -a4 crega4 -o #1.sys -y 0 psect debug crolib config -s syslib -s iolib mydriver ............... Of course, if your driver uses a major device number for which there are no device files available, you better use the Makdev utility to generate the device files; otherwise, there will be no way to access the new driver. 1.5 Debugging Mechanisms The standard crogen command file has the -d option provided. When this option is invoked, a map will be generated and the system debugger will be included in the code. (The files psect.o68 and debug.o68 must be in the current directory). When a Cromix-Plus file, which contains the system debugger, is booted, Cromix-Plus will come up with the debuuger prompt. If you type G (for Go), the boot procedure will continue and you will end up with a Cromix like any other except that it is some 20K bigger. Of course, before saying Go, you can install breakpoints, probably in you driver. That way you can debug it. We are using another very convenient debugging mechanism. A simple hardware modification can be made to connect a switch to the NMI (nonmaskable interrupt) pin. Cromix plus has a dummy NMI service routine at address 404H. If you install just one breakpoint, at 404H, any timy you generate an NMI the debugger will come up, independent of what Cromix, or user program, is doing, provided of course, that the debugger has not been destroyed due to some bug. At that point, a real breakpoint can be installed wherever required. The NMI story has a small inconvenience. It may happen that the Z80 processor will receive the NMI. If this happens, Z80 will just say "Runaway program aborted" the next time it is activated, with no other ill effect (unless the aborted program did some damage). There are two ways to fight this problem: - Build a dummy Z80 program and execute it after any activation of the NMI. There will be certainly no damage if a dummy Z80 program gets aborted. - Disconnect Z80 NMI pin. This is not simple as the NMI signal comes from the bus first to Z80 and then from Z80 to 68000. (One solution, if you do not mind, you can bend the Z80 NMI pin out of the socket, thereby disconnecting it while keeping the connection from the bus to 68000). Another, independent, debugging mechanism is the d_printf function. Of course, drivers cannot use system calls to do IO, e.g. printf function. The d_printf function is completely equivalent to Cromix system call _printf and is very simmilar to normal C printf function. The function will use the raw driver to write the characters out, so there is no way to tell where the output will go. The first (or only) argument must be the format string, the remaining arguments, if any, are the arguments to be printed (See jsys _printf system call). As C always pushes four bytes per argument, the format string must always say that arguments to be printed are long, i.e., use the formats %ld, %lu, %lc, %s (no letter ell here), e.g. d_printf("My driver, character %02lx read\r\n", ch); Note also that the raw driver knows nothing about CRDEVICE, PAUSE, etc, so that each line must be terminated by a \r\n, the \n alone won't do. Also, there is no CNTRL-S to stop printing. It will just run off the screen. If you hate the debugger this way of debugging is quite handy, provided you do not mind a lot of compiling and linking. The d_printf function is also used by most of the standard block device drivers to report fatal errors like disk IO errors. As a help in debugging, the Convobj utility creates the .cmp file which contains the addresses of all funtions, including static functions, relative to the beginning of the module. The address of the module is always given in the map generated (if requested) by the Link68 utility. 1.6 Summary of supporting software To write Cromix-Plus drivers you need the following supporting software c.bin C Compiler, version 02.41 or later code.bin Code Generator, version 02.41 or later asm.bin Cromemco Assembler, version 01.14 or later maklib.bin Library Manager, version 00.03 or later convobj.bin Obj Converter, version 30.03 or later link68.bin Cromemco Linker, version 00.23 or later debug.o68 System Debugger psect.o68 Psect definition if debug.o68 is to be used 2. Driver types, Functions, and Modes 2.1 Character drivers Each character driver must provide a function table of the form static int init(), open(), close(), read(), write(), gmode(), smode(); chd xxxxxx_c { init, open, close, read, write, gmode, smode }; The name xxxxxx should contain up to six characters in lower case. Xxxxxx is the name entered in the sysdef file. All driver functions except init are supposed to be reentrant. Cromix may decide to call another or the same function on any device even before the previous operation has been completed. If the driver cannot handle multiple requests it must put latecomers to sleep and wake them up when the driver can service them. The driver functions will be called in supervisor mode with the interrupt level set to zero (normally). If the driver has to do anything with interrupt level it must restore it when done. The operation of the function table is explained below. 2.1.1 init static int init(majdev,cnt,val) unsigned char majdev; int cnt, val[]; This routine will be called during the boot procedure in supervisor mode with interrupts disabled. Its argument is Cromix's notion of the major device number this driver belongs to. The major device number is given as an argument here so that init may construct the high order word of event numbers for all future needs by the driver (See section 3.10). The function is supposed to do all required initialization of the device(s). The function returns zero. It it cannot initialize the device it returns ERR whereupon the boot procedure will be aborted. The array int val[] with cnt components contains the numbers defined in sysdef file. The interpretation of these values, if any, is left to the driver. A typical use might be the list of minor device numbers the driver is supposed to serve. 2.1.2 open static int open(devn) unsigned short devn; The argument devn is the full device number. This function will be called anytime a user program tries to open a character device with this major device number. Minor device number interpretation is left to the driver. The driver should inspect the minor device number and decide if it is going to support it and, if so, whether there are any special features to be defined by the minor device number. For example, for serial printer driver, the high bits of the minor device number define the communication protocol. The same device may be opened more than once before it gets closed. The driver should maintain the open count for each device. As long as the open count is nonzero any number of read, write, gmode, or smode calls may come in. If, before opening, the open count is zero, the driver should probably do no extra processing. However, if the open count is zero there might be some processing required, e.g., to verify the existence of the device. Also, if mode DISCARD was set when the device was last closed (or if it first open at all), the mode values have to be set to some default values. The function should return zero. If a device cannot be opened the function should return ERR and the error number in extern unsigned err. 2.1.3 close static int close(devn) unsigned short devn; This function undoes the open function. If the driver counts opens up and closes down (for each device separately), as long as the count is nonzero a new request may come in. If it is not the last close no special processing is required. The last close, however, should flush any queues, and in case of mode DISCARD, notify the driver that the next open should reinitialize the mode values. The function should return zero. In case of error the function should return ERR and error number in extern unsigned err. 2.1.4 read static int read(devn,arg) unsigned short devn; char *arg; This is the reading routine. The argument devn is the full Cromix device number; arg is a pointer to a structure in the Kernel which keeps track of count and location of transfered characters. The driver need not know details of the structure, therefore arg may be declared simply as a character pointer. The driver gets this value as an argument, and passes it as an argument again to the Cromix function ioputc. The read function must read a character from the device, call function ioputc (section 3.5) to pass the character to the Kernel, and continue in this way until ioputc returns ERR (which means that the Kernel needs no more characters from this call). The function should return zero when it has no more characters to send to the Kernel or when the Kernel says it does not need them any more. If the function detects an error it must return ERR and error number in extern unsigned err. Most likely, the driver will be interrupt driven. In this case, as characters are typed in the interrupt service function must pick them up and store them in a queue as the Kernel might not want them right away. The read function will pick up the characters from the queue and send them to Kernel using the ioputc function. If the Kernel wants more characters and none is waiting in the queue, the read function should go to sleep to be awoken by the interrupt function when a new character is ready to be read from the device. The queue itself can maintained by the Kernel, the driver should only provide the header of the queue structure (see section 4.15). The modes will dictate a lot of character processing. For the indirect description of character processing consult the modeequ.h or the tmodeequ.h files (see sections 4.13 and 4.18) and the help file for the Mode utility. Complicated character processing might require another intermediate character queue so that, in general, the character flow is device --> type_ahead_queue --> input_queue --> ioputc 2.1.5 write static int write(devn,arg) unsigned short devn; char *arg; This is the writing function. The argument devn is the full Cromix device number; arg is a pointer to a structure in Cromix which keeps track of count and location of transfered characters. The driver need not know details of the structure, therefore arg may be declared simply as a character pointer. The driver gets this value as an argument, and passes it as an argument again to Cromix function iogetc. The function write must extract characters from Cromix by calling the routine iogetc (section 3.4) until iogetc returns ERR which means there are no more characters to be written. If the driver cannot write the characters immediately it must go to sleep and be awoken by the transition to ready interrupt. The function returns zero when it has completed its task. If the function detects an error it must return ERR and error number in extern unsigned err. It is not desirable for the write function to spend a lot of time sleeping and waiting for the device to be ready to accept the next character. A beter way is to organize another queue. The write function then simply stores the characters in the output queue, the interrupt service function can pick them up at its own rate. Again, the modes can dictate a lot of character processing which is the responsibity of the driver. 2.1.6. gmode static int gmode(devn,mode,val) unsigned short devn; int mode, *val; This is the getmode function. The meaning of mode numbers is driver dependent though similar drivers should use the same value for the same purpose (e.g. all terminal drivers must use the same values). The devn argument is the full Cromix device number; mode is the mode number whose value is to be returned in *val. The function returns either ERR (and error number in extern unsigned err) or one of the values listed below 1 mode value is one byte 2 mode value is two bytes 4 mode value is four bytes If jsys #_getmode was executed from a 68000 program this return value has no meaning (the driver must always return an int in *val). In case of a call from a Z80 program the Z80 simulator must decide how to store the mode value returned into Z80 registers. The simulator will store them as follows 1 reg_D = *val & 0xff 2 reg_DE = *val & 0xffff 4 reg_DEHL = *val If you are writing a driver which may be called from an existing Z80 program you should return the correct value so that the Z80 program will find the mode value in the register where it expects it (in most cases in the D register, i.e., the value returned should be 1). 2.1.7 smode static int smode(devn,mode,val,mask) unsigned short devn; int mode, *val, mask; The meaning of devn and mode is the same as for the getmode function. The value mask is used when just bits are to be set. The argument *val is more complicated since setmode must work for Z80 and 68000 programs. The mode value can be 1, 2, or 4 bytes, in the low order bytes of a long word if coming from the 68000, and arranged differently if comming from the Z80. In general, if the new mode value is coming from Z80 it will be loaded into register(s) D if one byte DE if two bytes DEHL if four bytes. To collect all possibilities, the Z80 simulator will asume that in each case the new mode value was loaded into DEHL and that value will be passed to the driver. As far as the driver is concerned it will get new mode in this way From 68000 +------+------+------+------+ | ???? | ???? | ???? | byte | if one byte +------+------+------+------+ +------+------+------+------+ | ???? | ???? | word | if two bytes +------+------+------+------+ +------+------+------+------+ | long word | if four bytes +------+------+------+------+ If the call is coming from the Z80 processor, the value of *val will be +------+------+------+------+ | D | E | H | L | in every case +------+------+------+------+ Now, if the driver knows which processor the call is comming from, it can rearrange the incoming data into a uniform value: +------+------+------+------+ | new mode value | in every case. +------+------+------+------+ The Kernel cannot do this transformation as it does not know what the mode numbers mean, therefore the Kernel does not know how many Z80 registers the user program loaded. The question to be settled now is how can the driver tell which processor the call is coming from. As the mode number from a 68000 program is a short value (2 bytes) the driver can make the decision as follows: if high order word of mode number is zero the call is coming from 68000, if the high order word is nonzero, the call is coming from Z80. The system call handler will ensure that the high order word will be set accordingly. After making a decision about the processor, the driver must discard the high order word of the mode number, transform the mode value if necessary, and do the job. When the call comes from the Z80 processor, the nonzero high order byte is not just any value. It is the bank number (a bank is 64K) of the bank where the Z80 program is running. This information is there for the unlikely event that the driver might need it. The last argument, mask, is the the value of E register, if coming from Z80 the value of D4 register, if coming from 68000 The old mode value returned is handled the same way as described above for the gmode function. 2.2 Block device drivers Each block device driver must provide a function table of the form static int init(), open(), close(), doio(), gmode(), smode(); blk xxxxxx_b { 0, 0, init, open, close, doio, gmode, smode }; The name xxxxxx should contain up to six characters in lower case. Xxxxxx is the name entered in the sysdef file. All driver functions except init are supposed to be reentrant. Cromix may decide to call another or the same function on any device even before the previous operation has been completed. If the driver cannot handle multiple requests it must put latecomers to sleep and wake them up when the driver can service them. The driver functions will be called in supervisor mode with the interrupt level set to zero (normally). If the driver has to do anything with the interrupt level it must restore it when done. 2.2.1 init static int init(majdev,cnt,val) unsigned char majdev; int cnt, val[]; This routine will be called during the boot procedure in supervisor mode with interrupts disabled. Its argument is Cromix's notion of the major device number this driver belongs to. The major device number is given as an argument here so that init may construct the high order word of event numbers for all future needs by the driver (see section 3.10). The function is supposed to do all required initialization of the device(s). The function returns zero. It it cannot initialize the device it returns ERR whereupon the boot will be aborted. The array int val[] with cnt components contains the numbers defined in sysdef file. The interpretation of these values, if any, is left to the driver. A typical use might be the list of minor device numbers the driver is supposed to serve. 2.2.2 open static int open(bp,mode) buf *bp; int mode; The arguments are a dummy block pointer and the open mode. The block pointer is provided so that the open function can generate the device number from it, has an available buffer to do any I/O if required, and to store the error number into bp->bf_errc if an error occurs (see section 2.24). The mode value specifies some additional conditions. This function will be called anytime a user program tries to open a block device with this major device number. Minor device number interpretation is left to the driver. The driver should inspect the minor device number and decide if it is going to support it and, if so, whether there are any special features to be defined by the minor device number. For example, for the uniform floppy driver, the high bits of the minor device number define the size, double sided and double density attribute, and so on. The open routine must return an error only in the case the device is unreadable or nonexistent. If it is not a Cromix mountable device it is of no concern to the driver. That matter will be handled in Cromix by the mount system call. If the device is usable at all the driver must be able to open it. The same device may be opened more than once before it gets closed. The driver should maintain the open count for each device. As long as the open count is nonzero any number of doio, gmode, or smode calls may come in. If, before opening, the open count is zero, the driver should probably do no extra processing. However, if open count is zero there might be some processing required, e.g., to verify the existence of the device, to compute the device size, and so on. The function should return zero. If a device cannot be opened the function should return ERR and error number in bp->bf_errc. If mode&OM_MOUNT is set the Kernel is informing the driver that it is mounting the device (mount system call). If mode&OM_FORCE is set, the driver should open the device only formally, not doing any actual IO. This form of open is used only by the programs like Initflop. The Initflop utility will use only the primitive smode functions to initialize the device. In short, the open function should only ensure that the smode calls for initializing will work. The initializing utility will not expect that the driver knows any particular physical characteristics of the device as these are to be determined later. 2.2.3 close static int close(bp,mode) buf *bp; int mode; This function undoes the open function. If the driver counts opens up and closes down (for each device separately), as long as the count is nonzero a new request may come in. The function returns zero. In case of error the function returns ERR and error number in bp->bf_errc. The buffer is again supplied for the same reason as in the open function: to provide device number, to give room for error code, and to offer buffer space in case the driver needs it. If the mode&CM_UNMOUNT is nonzero the Kernel is informing the driver that it is unmounting the device (unmount system call). In mode&CM_EJECT is nonzero the driver should try to physicaly disengage the device (eject in case of PERSCI floppy drives. Before returning from the last close the driver should really close the device in the sense that all traces of device usage are removed: flush internal buffers, and so on. If the device is removable the user should be allowed to remove it after the last close. 2.2.4 doio static int doio(bp) buf *bp; This function is responsible for reading and writing. All input data is recorded in the buffer structure: bf_read 1 = read, 0 = write (Read only) bf_devn device number (Read only) bf_block block number to be transfered (Read only) bf_addr pointer to actual buffer (Read only) bf_errc error number in case of error (Write only) bf_cylinder scratch room for the driver (Read/write) bf_surface scratch room for the driver (Read/write) bf_sector scratch room for the driver (Read/write) bf_count scratch room for the driver (Read/write) The remaining components of the buffer structure are off limits. When the operation is completed the function iodone(bp) should be called to notify the kernel. The process that is waiting for the I/O is asleep and the iodone function will wake him up. This means that the driver might postpone the actual execution of I/O and return more or less immediately (e.g., after checking the block number is in range), without calling iodone. Of course, the iodone function must be called sooner or later otherwise the system might lock itself up. To help the driver with such postponed execution the kernel contains the bfqueue and bfunqueue functions which handle the queue of waiting IO requests for each driver (see sections 3.14 and 3.17). Driver must provide only the queue header iobuf (see section 4.10) and call the bfqueue function to enter the buffer into the queue. The bfqueue function will sort the queue, first read requests, then write requests. Within each category the bfqueue function will consult the compare function, supplied as the argument, to decide in what order should the requests be queued. The interrupt mechanism must then call the bfunqueue function to get the next request from the queue, process it, call iodone function (see section 3.16) when it is done, unqueue next request, and so on. Smode or other calls (except doio) may come in between the queued requests. The driver should service the unqueued requests first, ant then, when no more are pending, start the interrupt driven queue processing. Of course, if the driver needs no waiting for real time events, e.g. amem driver, it can handle each request as it comes in, execute it, call iodone, and return. The doio function should return zero; any error should be returned in the bp->bf_errc. 2.2.5 gmode static int gmode(devn,mode,val) unsigned short devn; int mode, *val; The meaning of gmode function is partially driver dependent. The first few modes are standard and can be handled by the bu_getmode function (see section 3.26). If the driver supports specific modes, it should follow these rules: In case of error, return ERR and error number in the extern unsigned variable err. Otherwise return one of the following 1 mode value is one byte 2 mode value is two bytes 4 mode value is four bytes (See the discussion of character driver gmode function). The mode numbers supported by the standard drivers are BMD_STATUS Return 1 byte, see bmodeequ.h (4.3) BMD_FLG1 Return 1 byte, see bmodeequ.h BMD_FLG2 Return 1 byte, see bmodeequ.h BMD_FLG3 Return 1 byte, see bmodeequ.h BMD_SIZE Return 4 bytes, number of blocks BMD_VERSION Return 2 bytes, version number BMD_RPM Return 2 bytes, rotation speed 2.2.6 smode static int smode(devn,mode,val,mask) unsigned short devn; int mode, *val, mask; The meaning of smode function is partially driver dependent. The first few modes are standard and can be handled by the bu_setmode function (see section 3.27). If the driver supports specific modes, it should follow these rules: In case of error, return ERR and error number in the extern unsigned variable err. Otherwise return one of the following 1 mode value is one byte 2 mode value is two bytes 4 mode value is four bytes (See the discussion of character drivers smode function). The mode numbers supported by the standard drivers are BMD_STATUS Return 1 byte, see bmodeequ.h (4.3) BMD_SEEK Primitive seek BMD_INIT Initialize track BMD_PRDWRT Primitive read/write BMD_RDWRT Block read/write BMD_PHYCHAR Return physical characteristics BMD_LDFIRM Download firmware The last six values are in fact complicated special driver functions. The val argument for these calls is not an int but a pointer to a structure which contains additional information. See bmodeequ.h file for additional information. 2.3 Raw Drivers Cromix uses the raw console for I/O when the normal console driver is not available. The pointer to a raw driver is specified at Crogen time. The raw driver for a particular device must declare just one entry point static int init(), input(), output(); raw raw_driver_name = { init, input, output }; 2.3.1 init static int init(cnt,val) int cnt, val[]; The init function will be called immediately after boot, in supervisor mode, with interrupts disabled. Its purpose is to perform all actions (which may be none) to make raw input and output routines work. The function should return value zero. The Sysdef file defines a number of integer arguments to be passed to the init functions. Cnt will be the number of arguments and val[] will be the array of these arguments. Actual meaning of these arguments, if any, should be defined by the driver. 2.3.2 input static int input() The input function should read a character from the raw terminal and should return that character (without the parity bit). There is to be no processing (no echoing, CR transformations, etc.). Typical code: wait in tight loop until a character is ready get the character from I/O port return character masked with 0x7f. 2.3.3 output static int output(c) int c; The output function gets a character as an argument. It must write it to the raw terminal, again without any processing. The function returns zero. Typical code: wait in tight loop until port ready output a character return zero 3. Support Functions from the Cromix-Plus Kernel Here is the list of functions, resident in the kernel, that the drivers might use. Any other function must be provided by the driver itself. 3.1 setlev and reslev int setlev(level) int level; and reslev(oldlev) This pair of functions (setlev is actually a macro which calls function _setlev) is used to change the interrupt level of the processor. The function setlev(level), 0 <= level <= 7, will set the interrupt level of the processor to level. The setlev function will return the previous status register. The returned value is usually saved to be used as the argument to reslev function which just loads the status register to its argument. Interrupt level zero means all interrupts are enabled, interrupt level seven means all interrupts are disabled. Most devices generate interrupts on level one. If the interrupt level is set to lev, all interrupts on level less than and equal to lev are blocked. These functions should be used very carefully. Important: to keep the code well organized a function which changes the interrupt level should have the following general layout: { int oldlev; ........... oldlev = setlev(newlev); ...... reslev(oldlev); ............ } In general, each driver function should return with the interrupt level set to the incoming value. A driver can set the interrupt level to some nonzero value and go to sleep. The scheduler will enable the interrupts so that the interrupt the driver is waiting for will really happen. The interrupt service function should then wakeup the mainstream of the driver and return. The mainstream driver function will resume processing, the interrupt level set to the value it had before going to sleep. 3.2 addint int addint(port,routine) unsigned char port; int (*routine)(); The function addint should be called from init routines (if at all). Its purpose is to install interrupts. port Interrupt port number (ipage offset) in Z80 terminology. routine Function to be called when an interrupt on that port happens Port number given must be even except for the following 0xc7, 0xcf, 0xd7, 0xdf, 0xe7, 0xef, 0xf7, 0xff. This also means that even addresses one above and one under those eight are not available: 0xc6, 0xc8, 0xce, 0xd0, 0xd6, 0xd8, 0xde, 0xe0, 0xe6, 0xe8, 0xee, 0xf0, 0xf6, 0xf8, 0xfe, 0x00. The function addint returns zero. If an interrupt with a different service function has already been installed for such a port the function returns ERR. The init function should then return an ERR whereupon the boot procedure will be aborted: the drivers obviously cannot agree which interrupt will be serviced by whom. The system does all the housekeeping for each interrupt. When an interrupt occurs the appropriate routine will be called (with its own vector number as the argument). The following assertions are true when an interrupt function is called: Interrupt level is set to the level device belongs to (level one for all present hardware). Interrupt level should not be changed (or if changed it must be restored to the entry level). Interrupt routines get their own stack (1K). Be careful not to overrun it. Interrupt routines must return zero. If an interrupt routine returns nonzero, a process switch will occur after the interrupt has been serviced, provided the interrupt occured in the user's program. 3.3 addauto int addauto(level,func,stack) int level, (*func)(); short *stack; Install an autovectored interrupt of given level. Func is the interrupt routine which will be called with no arguments, stack is the initial stack pointer to be used by the interrupt routine belonging to that autovectored interrupt. The function should not enable its own interrupt level, it should return normally. The interrupt routine will be called in supervisor mode using the supplied stack. The function addauto returns zero. If the interrupt address is occupied it will return ERR. At present there are no drivers which would use the autovectored interrupts. In the future we expect the SMD hard disks to use one of them. 3.4 iogetc int iogetc(arg) char *arg; This routine should be called from the write routines of character drivers whenever they need a new character to be sent to the device. If iogetc returns ERR there are no more characters to be written with this call. If iogetc does not return ERR it returns the character to be written out. Argument to iogetc is in fact not a character pointer but a pointer to a structure where Cromix keeps information about the characters to be written out. The value of arg is given to the driver with each write call. The driver write function should keep writing as long as iogetc is giving it characters. As an alternative, the write function can quit earlier by returning an error (e.g. device walked away). 3.5 ioputc int ioputc(c,arg) int c, *arg; This function will send the character c to Cromix (arg identifies what Cromix is supposed to do with it; the character driver gets the value arg with each read call). The function ioputc returns zero. If it returns ERR it means this was the last character Cromix was expecting from the driver. The character driver should not call ioputc again (until the driver read function is called again). The driver may quit earlier, e.g. CNTRL-Z, and signal an error by returning ERR. 3.6 putq int putq(cbp,c) cbuf *cbp; int c; If the driver was forced to accept a character (e.g. from interrupt routine) it must store it somewhere until appropriate read call is made from Cromix. The driver should not allocate a buffer to do it (unles it is really small) but should use the system buffers instead. Function putq is used to store characters in the system buffers. To use it the driver must declare the structure cbuf (one for each possible queue). To initialize the queue say static cbuf queue; cbuf.cb_cnt = 0; or equivalent. This means the queue is empty. Whenever you want the system to save a character for you use the function putq, where the first argument is a pointer to cbuf structure, the second argument the character to be stored. The function will return zero unless there is no room in system buffers. In this case it will return ERR. 3.7 getq int getq(cbp) cbuf *cbp; Function qetq extracts a character from the system queue identified by the pointer cbp on first in first out basis. The function will return the character stored. If the designated queue is empty the function will return ERR. 3.8 unputq int unputq(cbp) cbuf *cbp; The function unputq extracts a character from the system queue identified by cbp on the last in first out basis. The function will return the character retrieved. If none are stored the function returns ERR. 3.9 sleep int sleep(event) unsigned event; If the driver finds out it cannot proceed immediately it can go to sleep waiting for the event defined by event. While a process is running in a driver (more generally, while a process is executing a system call) Cromix will never put it to sleep due to timer events. Instead, the process must swap out itself by going to sleep. The driver will not come out of the sleep function until -- it is awakened by the required event -- it is awakened by any signal The function sleep returns zero if awakened by the event, ERR if awakened by a signal. Sleep is a general Cromix mechanism used in many places. Outside the drivers there are different processes which wakeup other processes when appropriate. Within the drivers it is usually an interrupt routine which is responsible to wakeup a driver. As the interrupt might occur at any moment, even while process is preparing to go to sleep, it might miss the wakeup event and then sleep forever. So, if an interrupt is supposed to wake up a sleeping process, the process must disable the interrupt level the interrupts are coming from and then call the sleep function. Sleep function will put the process to sleep, enable all interrupts and then wake up some process (the same one or a different one) that is ready to run. When the current process is awakened, it will come to life at the scheduler's discretion, and continue execution as if nothing happened. Interrupt driven parts of the driver may wish to go to sleep while waiting for an interrupt. If the sleep function returns a nonzero value it means that sleeping was interrupted by a signal. The signal is still waiting in the process tables so no particular processing is required. However, the driver has two possibilities: It can abort whatever is doing by saying err = _ssignal (aborted by signal) and returning ERR. Another possibility is to ignore the presence of the signal by going back to sleep. This means that if the driver wishes to ignore the signal it may do it but it must verify if reason for sleep still exists and if so, it must go to sleep again. Note that processing of queued requests should not be aborted by a signal: as far as the Kernel is concerned, the IO operation was completed the moment the driver function returned. The fact that the actual IO is postponed is known only to the driver. If the driver cannot complete the operation, it can only write the _ioerror number into bp->bf_errc and call the function iodone. In a read operation, the kernel will probably try again because the presence of an error as it needs the information. In a write operation the kernel believes the data was written out. This means that errors caused by queued IO must unconditionally be reported to the raw console. Except for the above case, the driver should respect incoming signals; this allows thr user's abort keys to save the driver from an infite wait loop. Note that once a signal is received, additional signals will be ignored (except for sigkill) so that the driver will not be awakened again. An addititional issue is the handling of large read and write sequences in the character drivers. Since the driver cannot tell in advance how many characters it will have to transfer with one call, it is advisable for the driver to check for presence of signals without waiting for the end of transfer. Otherwise, a lengthy call wil not receive a waiting signal unless it happens to go to sleep (which might not happen if the recieving device is fast enough). If the sgtest function says a signal is waiting, the driver should abort whatever it is doing, say err = _ssignal (aborted by signal) and return ERR. This will cause the system call to be aborted immediately, the caller will either be aborted or, if he has a trap routine for the signal, will be told that system call was aborted by a signal. As there are many events Cromix is waiting for, they are carefully organized into groups. 31 24 23 16 15 0 +-------+-------+-----------------+ | group | major | driver's reason | +-------+-------+-----------------+ Group 0 is reserved for kernel, group 1 for character drivers, group two for block device drivers. Next, the major byte should give the major device number as obtained from Cromix as part of devn argument. The initialization call gets the major device number as an argument so it can build static unsigned event; event = CHARDEV | major << 16; and then take event and OR into it the driver's reason for sleep (16 bits). If this scheme is adhered to it ensures that one part of Cromix will not be awakened inadvertedly by another. Drivers can also use the kernel's group of events. Whenever the Kernel invents an event it is always the address of some data structure the Kernel knows about (addresses have high byte zero so this is the Kernel group). Drivers can also use addresses of data structures, provided they are local to the driver so that the Kernel cannot know about them. 3.10 wakeup wakeup(event) unsigned event; This function will wake up all processes which are waiting for the specified event. See the description of sleep function. Waking the process up does not mean the process will immediately resume execution. It just means that the process becomes eligible for the scheduler to choose it at some later time for continued execution. In the drivers, the wakeup call is often used by the interrupt routines to notify the mainstream of the driver that the specified event has actually happened. The driver had better take all precautions that the event, the process is waiting for, will actually happen (time outs!) otherwise the process may become The Sleeping Beauty, with no prince to wake her up, except for the ugly old witch called RESET. 3.11 sgtest int sgtest() Before going to sleep a driver might wish to check if a signal is waiting there already. Function sgtest returns zero if no signal is waiting, ERR if a signal is waiting. No signal processing is done. The importance of such checking lies in the fact that additional signals will not be accepted and the driver will not be awakened by them. If a driver intends to abort his processing on an incoming signal, it should check for waiting signals and abort immediately instead of going to sleep. 3.12 sgttysig sgttysig(signo,ctty) int signo, ctty; This function will send signal number signo to all procsses which have device ctty as the controlling terminal. Also, if the signal number is sighangup, the signal will be sent to all processes that explicitly requested it (the Kernel keeps track of it). Note that the driver may, but need not be able to, detect modem hangup condition. If so, and if the mode was set to SIGHUPALL, this routine must be called. In addition, if HUPENABLE is set on last close the driver should drop the phone line. 3.13 -- deleted -- 3.14 d_printf int d_printf(format,arguments) char *format; The function d_printf is a general printf routine set up to write on the raw console. It can be used to report unexpected errors like hard CRC errors and the like. In general, use it whenever the error code return is not descriptive enough. D_printf behaves like C printf function with the following differences: It will always write on the raw console. It can accept any number of arguments, but being implemented with the same mechaninsm as jsys _printf, and being called from C where all arguments are four bytes, the format descriptors must say that arguments are long, e.g., %06ld instead of %06d As the raw console is used there is no character processing which means lines should be terminated by \r\n insted of \n alone. The function d_printf can also be used as a debugging mechanism. 3.15 iodone iodone(bp) buf *bp; The block device driver function doio must call the iodone function whenever an operation is completed, with an error or without it. The iodone function will notify processes the the IO opration, they are waiting for, is completed. 3.16 bfqueue bfqueue(hp,bp,compare) iobuf *hp; buf *bp; int (*compare)(); The function bfqueue should be called from the block device doio function if it intends to do queued IO. The argument hp is the address of the queue header iobuf (declared by the driver). The bp argument is the buffer pointer of the request to be queued. The third argument is the compare function, formally declared as static int compare(bp,bq) buf *bp, *bq; This function will be called frem the bfqueue function whenever it must decide which of two queued requests should be served first. If the function returns < 0 bp first > 0 bq first = 0 first in first out After the request is queued, the doio function may return (zero). Of course, it is the drivers responsibility the the queue processing will start if it has not been started already; and it is the drivers responsibility to interrupt queue processing if an unqueued request is waiting, and to start queue processing afterwards. 3.17 bfunqueue buf *bfunqueue(hp) iobuf *hp; This function will extract the first queued request from the driver's queue headed by the iobuf structure. The function returns the pointer to first buffer to be serviced, or NULL if the queue is empty. This function is normally called from the interrupt routine after one queued request has been serviced, or from the mainstream driver when the queue processing has to be restarted. 3.18 lockev and unlockev lockev(event) short *event; unlockev(event) short *event; This pair of functions can be used a general semaphore mechanism. The short value, pointed to by the argument, should be considered as the the number of processes which require exclusive access to some system resource. When the call from function lockev returns, the caller has exclusive access to this resource (provided other potential users also use lockev/unlockev functions). When exclusive access is no longer needed, the unlockev function should be called to wakeup the processes that might have gone to sleep because the resource was locked. 3.19 fill fill(dest,length,c) char *dest; int length; char c; Fill length bytes starting at dest with the character c. 3.20 zap zap(dest,length) char *dest; int length; Equivalent to fill(dest,length,0). Note that zapping of static variables is not required: all static variables are initialized to zero by the rest of the system. 3.21 move move(to,from,length) char *to, *from; int length; Move length bytes from adress from to address to. The function is most effective if the both addresses are on even boundaries. Care must be exercized if the distination overlaps the source. Bytes are moved from left to right, in multiples of four if possible. 3.22 arithmetic operations This is the list of functions, provided in the kernel, and inteded to be used implicitly: the code generator will generate calls to these functions without the programmer being aware of it. These functions are: $I_MUL4 4 byte multiplication $I_DIV4 4 byte signed division $I_MOD4 4 byte signed remainder $U_DIV4 4 byte unsigned division $U_MOD4 4 byte unsigned remainder 3.23 debug() This, and the following functions, are provided in the blkutl.c file. This file is part of the iolib source system. The functions listed here are used only by the drivers. The function debug is a dummy function provided here to be called at the beginning of the initialization procedure in case that the real system debugger is not provided. 3.24 lockdma and unlockdma lockdma() unlockdma() This pair of functions is intended for drivers like floppy drivers which must ensure that STDC will not generate a DMA transfer while floppy is in a time critical section of code. 3.25 bu_getmode int bu_getmode(dp,mode,val) dcb *dp; int mode, *val; This function provides the general getmode function for standard mode numbers: BMD_STATUS return dv_stat BMD_FLG1 return dv_flag1 BMD_FLG2 return dv_flag2 BMD_FLG3 return dv_flag3 BMD_SIZE return dv_maxblk*blocksize BMD_VERSION return current iolib version (defined in ioversion) For any other mode number the function says _badvalue. The above list need not be considered final. In any case the driver getmode function must service other mode numbers (if any) itself and can call bu_getmode to handle the standard ones. Note that bu_getmode relies on the fact that the dcb structure is properly declared and initialized. 3.26 bu_setmode int bu_setmode(dp,mode,val,mask) dcb *dp; int mode, *val, mask; This function provides the general setmode function for standard mode numbers: BMD_STATUS change dv_stat For any other mode number the function says _badvalue. The above list need not be considered final. In any case the driver setmode function must service other mode numbers (if any) itself and can call bu_setmode to handle the standard ones. Note that bu_setmode relies on the fact that the dcb structure is properly declared and initialized. 4. Header Files Most of the header files described in this section are imported form the Cromix-Plus source code. This means that they should be used whenever possible. Unless the file is unknown to Cromix-Plus it should not be modified as Cromix-Plus itself would not be aware of the change. The header files are described in alphabetic order. The header files not listed here are private to some drivers (or to a group of driver functions). They can be changed at will provided the modification will not cause some driver to be unusable. 4.1 BDRV.H This file describes the function table of boot drivers. The Iolib package contains also the drivers which are used during the boot procedure (they are incorporated in fdboot and simmilar files). The user has no need to change them as they are not a part of Cromix-Plus proper. The file Bdrv.h is known to boot programs. 4.2 BLOCK.H File Block.h defines the type of structure blk. Each block device driver is supposed to declare just one entry point blk driver_name = { 0, 0, name_of_init_routine, name_of_open_routine, name_of_close_routine, name_of_doio_routine, name_of_getmode_routine, name_od_setmode_routine }; Driver_name is the only name that Cromix is aware of. The driver name should be in style of the form xxxxxx_b. In the this scheme xxxxxx denotes any string of up to 6 characters, all in lower case. The name xxxxxx is the name the user must write into the Sysdef file. The extension _b will be added by the Sysdef utility that Crogen command file calls to compile the Sysdef file. Cromix will call the driver functions from the blk structure as described in section 2.2. Note that the driver routine gets its own device number passed as an argument. This allows an unlimited number of different drivers, each one using its own name to specify the blk structure. Actual device numbers will then be defined at crogen time, when up to bdevcnt drivers will be selected to denote major device 1, 2, ... bdevcnt. The file Block.h is known to Cromix-Plus. 4.3 BMODEEQU.H This file is an exact copy of the Bmodeequ.h file from the /equ directory. The file Bmodeequ.h is known to Cromix-Plus. 4.4 BUF.H As far as block device drivers are concerned the actual I/O is always done in blocks of blocksize characters. Each block device contains blocks block 0 block 1 block 2 ......... block maxblk-1 The limit maxblk is determined by the driver and is based on the actual device characteristics (size, density, label, ...). Each I/O request will ask to read or to write one full block. The I/O request will get a buffer pointer buf *bp as an argument. As described in section 2.2.4, the components include: bf_errc Output parameter. Zero on entry, (Cromix) error number (zero if no error) on exit. bf_read Input parameter. Has meaning only for bk_doio. Nonzero means read, zero means write. bf_devn Input parameter. Full device number. bf_block Input parameter. Block number. bf_addr Output if bf_read nonzero, input if bf_read is zero. This is actual buffer address of blocksize characters. bf_cylinder Scratch variable for the driver bf_surface Scratch variable for the driver bf_sector Scratch variable for the driver bf_count Scratch variable for the driver The file Buf.h is known to Cromix-Plus. 4.5 BUFSIZE.H The file Bufsize.h is logical part of the file Buf.h. It is formally separate as as lot of utilities want to know only the buffer size and are not interested the the details of the buffer structure itself. The file Bufsize.h is known to Cromix-Plus. 4.6 CHAR.H File Char.h defines the type of structure chd. Each character driver is supposed to declare just one entry point chd driver_name = { name_of_init_routine, name_of_open_routine, name_of_close_routine, name_of_read_routine, name_of_write_routine, name_of_getmode_routine, name_od_setmode_routine }; The driver_name is the only name that Cromix is aware of. The driver name should be in style of the form xxxxxx_c. In the this scheme xxxxxx denotes any string of up to 6 characters, all in lower case. The name xxxxxx is the name the user must write into the Sysdef file. The extension _c will be added by the Sysdef utility that Crogen command file calls to compile the Sysdef file. Cromix will call the driver functions from the chd structure as described in section 2.1. Note that the driver routine gets its own device number passed as an argument. This allows an unlimited number of different drivers, each one using its own name to specify the chd structure. Actual device numbers will be defined at crogen time, when up to cdevcnt drivers will be selected to denote major device 1, 2, ... cdevcnt. The file Char.h is known to Cromix-Plus. 4.7 DEVICE.H A block device driver will probably have to keep some information about each device it is supporting. The structure dcb defined in this header file will be of some help. Its use is mandatory if the common buffer routines are to be used. Dv_maxblk is actually the number of blocks on the device. This means that the driver can handle blocks 0, 1, ..., dv_maxblk-1. Dv_other is declared to be a character pointer though the idea is that dv_other is pointing to additional information related to a device, its precise structure being driver defined. Cromix-Plus does not know anything about Device.h file. 4.8 ERROR.H This header file defines Cromix error numbers. For those error numbers, and for those only, Cromix jsys #_error knows the error messages. Any other error number returned will cause the error message "Unknown error number" if the user is going to write it out. Note that error number symbols start with the underscore character as the question mark character has a different syntactic meaning in C. This leads to some confussion as the Cromix system call numbers also start with the underscore character. In particular, there is jsys #_signal system call and _ssignal error number (note double s). The file Error.h is known to Cromix-Plus. 4.9 EVENT.H In case drivers need to go to sleep waiting for an event, an event number for sleep (and wakeup) must be provided. Event numbers are arbitrary 32 bit numbers. Of course, it is mandatory that different events do not get mixed up. The Kernel is uses addresses of data structures as event numbers. The drivers can do the same as long as the data structure is private and not known to the Kernel (buffer pointer, for example, won't do). An alternative for a driver is to contructs special event numbers according to the following scheme 31 24 23 16 15 0 +------+-------+----------------+ | type | major | driver defined | +------+-------+----------------+ For events generated by character drivers CHARDEV value must be used as type, for block device drivers the BLOCKDEV value must be used as type. Also, major device number (Cromix defined) must be ORed into bits 16-23. The event numbers built this way are reserved for that particular driver. The low order 16 bits are reserved for the driver itself. They can be anything. A driver is notified of its own device number at init time. If the driver cares it can build all event numbers at init (according to the above scheme) and use them later as required. Cromix-Plus does not know anything about Event.h file. 4.10 IOBUF.H Cromix-Plus supports queued IO requests. This means that when the Kernel calls a block device driver to read or write a block of data, the driver can enter the request into the driver's queue. Driver can then have an interrupt routine pulling out buffers from the queue and actually executing the IO. The buffer structure itself contains enough information to describe exactly what kind of an operation is required. The Kernel will do the queueing for the driver; the driver must only keep a private iobuf record that will serve as the head of buffer chain. The file Iobuf.h is known to Cromix-Plus. 4.11 IOLIB.H This file must be included in every C module. It will ensure that the module will be properly compiled. Cromix-Plus does not know anything about Iolib.h file. 4.12 MACROS.H This file contains macro definitions usually found in STDIO.H (which should not be used). In addition, the file defines error return value ERR and device number management macros. In Cromix, device number is an unsigned short value which is composed of the major and minor device numbers. Macro devnum(major,minor) will build Cromix device number out of major and minor device numbers, major(devn), minor(devn) will extract major and minor device numbers from Cromix device numbers. The file Macros.h is known to Cromix-Plus. 4.13 MODEEQU.H This is standard file in case you need it. The file Modeequ.h is known to Cromix-Plus. 4.14 PORT.H The file Port.h defines the base port numbers and interrupt numbers (in Z80 style) for all devices known to Cromix-Plus. The file Port.h is known to Cromix-Plus. 4.15 QUEUE.H This file contains definition of character buffer structure in case the driver needs Cromix to keep a (double ended) queue of characters for a driver. The driver is supposed to declare a (10 bytes) structure cbuf which will inform Cromix to maintain character buffers for the driver. Never touch those structures yourself. Cromix functions will do it for you. See sections 2.2.4, 3.16, and 3.17. The file Queue.h is known to Cromix-Plus. 4.16 RAW.H This file contains the definition of a pointer table raw which is simmilar to chd or blk except that it contains pointers to raw character drivers. Raw character drivers are supposed to be completely independent of normal character drivers. Each Cromix will contain the defininition of a raw console by pointing to appropriate raw drivers. The raw drivers will be used only during the boot procedure where the full drivers are not yet operable, or during a serious Cromix malfunction where regular drivers may not be operable any more or there is no user terminal which should recieve an error message, e.g., hard disk CRC errors. See section 2.3. The file Raw.h is known to Cromix-Plus. 4.17 SIGNAL.H The file Signal.h contains definitions if signal numbers. A character driver might need it to generate a signal when the driver detects that the user pressed the ^C key. The file Signal.h is known to Cromix-Plus. 4.18 TMODEEQU.H The file Tmodeequ.h is an extension of Modeequ.h file. It covers the definitions pertinent to Tape drivers. The file Tmodeequ.h is known only to the Mode utility and other Tape utility programs. 4.19 WATCHDOG.H The Scheduler function in the Kernel can maintain watchdog timers for the drivers. The idea is something like an alarm clock system call (execept that drivers cannot make system calls). Also, the watchdog mechanism has finer resolution (16 miliseconds) than the alarm clock mechanism. If a driver wants to use the watchdog mechanism, the driver must declare a wdg record. A call to callwd kernel function will startup the watchdog. Scheduler will call the function supplied int callwd call after the specified time elapses. Of course, a driver can cancel the watchdog before it goes off. The file Watchdog.h is known to Cromix-Plus.