TOC PREV NEXT INDEX

Writing Device Drivers for LynxOS


Entry Point Functions

This chapter describes the entry point functions and provides basic examples on their usage.

Entry Point Functions

The table below lists LynxOS entry point functions and summarizes their usage.

LynxOS Entry Point Functions
Entry Point Function
Description
install()
Initializes the hardware and allocates shared memory buffers.
uninstall()
Deallocates shared memory and clears used interrupt vectors.
open()
Initializes minor devices.
close()
Called only when the last open file descriptor pointing to a minor device is closed.
read()
Reads data from the device.
write()
Sends data to the device.
ioctl()
Executes a device-specific command.
select()
Supports I/O polling or multiplexing.
strategy()
Schedules read and write operations on a block device.
mmap()
Maps data to memory.

Required Functions

Not all entry point functions are required for a device driver. The functions to implement are determined by the inherent characteristics of the device, the device driver type (block or character), and whether the device driver is statically or dynamically installed. Following are some general guidelines.

Declaring the Entry Point Functions

The entry point functions are declared within the driver code module and their addresses are identified to the kernel upon driver installation. Because device drivers can be installed statically or dynamically, two distinct processes exist for registering the entry point function names with the kernel.

In a static installation, the driver code and data are incorporated into the kernel image. (This process follows the conventional UNIX model.) The entry point function names are also declared in a configuration file, which is subsequently incorporated into the kernel build process. Device driver installation is covered in detail in "Installation and Debugging"

A device driver can also be installed dynamically using the drinstall program or the dr_install() system call. A data structure named entry_points is used to pass the addresses of the entry point functions to the kernel. This data structure is initialized within the driver code module. Its type is dldd, which is defined in <dldd.h>. The dldd data structure is described in "Device Driver Basics" Device driver installation is covered in detail in "Installation and Debugging"

install()

The install() entry point function is invoked each time the device driver is installed for a major device. This entry point is responsible for initializing the major device (see "Major and Minor Device Designations"). Block- and character-type device drivers use slightly different versions of install().

For character-type device drivers, the prototype for install() is:

char *install(devinfo *info)

where:

info Is a pointer to a device information structure

For block-type device drivers, the prototype for install() is:

char *install(devinfo *info, statics *s)

where:

info Is a pointer to a device information structure
s Is a pointer to a statics data structure

info points to a device information data structure. (See "Device Information Data Structure".) This data structure characterizes the major device that the instantiation of the driver supports. Typically, this structure holds configuration information such as IRQ level, I/O address, or available resources. The device driver uses this information to initialize the major device.

At a minimum, the install() entry point should initialize the major device and in character-type device drivers, and allocate memory for the statics data structure. (See "Statics Data Structure".)

The install() entry point should also initialize the statics data structure, register the interrupt handler and kernel threads. The data received from the device information data structure should also be copied to the statics data structure for use by the other entry points, if appropriate.

The install() entry point returns either SYSERR or a pointer to a statics data structure. If a bus error occurs while install() is being executed, it is automatically aborted with the same effect as if a SYSERR has been returned.

install() Example

In the example below, the dev_install() routine checks for the existence of the device using device_is_present() (which is a supporting routine located elsewhere in the driver code). If the device is present, the statics structure (s) is allocated and initialized; otherwise dev_install() returns SYSERR.

Next, dev_install() attempts to initialize the hardware with the supporting routine device_init(). If the initialization succeeds, dev_install() returns the address of the statics structure; otherwise, the statics structure is deallocated and dev_install() returns SYSERR.

char *dev_install(devinfo *info)
{
struct statics *s;
int error_found = 0;

/* Check for existence of device. If present
allocate and initialize statics struct. */
if (device_is_present(info))
{
s = (struct statics *)sysbrk((long)sizeof(*s));
bzero(s, sizeof (*s));
s->io_addr = info->io_addr;
s->intr_vec = info->intr_vec;
...
}
else
return ((char *)SYSERR);

/* Initialize device */
error_found = device_init(s);
if (error_found)
{
sysfree(s, (long) sizeof (*s));
return ((char *)SYSERR);
}
else
return ((char *) s);
}

uninstall()

The uninstall() entry point function is invoked when a device driver is dynamically removed from the system by way of the devinstall system command or the cdv_uninstall() or bdv_uninstall() system calls.

The prototype for uninstall() is:

int uninstall(statics *s)

where:

s Is a pointer to a statics data structure

The uninstall() entry point must free up the statics structure, s, (see "Statics Data Structure") and any other resources allocated or set by the device driver. Dynamically allocated memory for data structures or queues must be deallocated and all used interrupt vectors must be cleared. Also, if applicable to the hardware, an attempt should be made to put the device into an inactive state. The uninstall() entry point must return either OK or SYSERR.

uninstall() Example

In the following example, the dev_uninstall() routine first attempts to put the hardware into an initialized state with the device_init() (a supporting routine located elsewhere in the driver code). Next the interrupt vector used by the device driver is cleared and the statics structure is deallocated.

If device_init() succeeds, dev_uninstall() returns OK; otherwise it returns SYSERR.

int dev_uninstall(statics *s)
{
int error_found = 0;

/* Attempt to put the device into an initialized state */
error_found = device_init(s);

/* clear interrupt vector then deallocate statics structure*/
iointclr(s->vector);
sysfree(s, (long) sizeof(*status));

if (error_found)
return SYSERR;

return OK;
}

open()

The open() entry point function is called in response to each open system call made by an application. It is used to perform minor device initialization (see "Major and Minor Device Designations") and can also be used to register ISRs and kernel threads.

The prototype for open() is:

int open(statics *s, int devno, file *f)

where:

s Is a pointer to a statics data structure
devno Contains the major and minor device numbers
f Is a pointer to a file structure

For every minor device accessed by the application program, the open() entry point of the driver is accessed. Thus, if synchronization is required between minor devices (of the same major device), the open() entry point handles it. Every open() system call on a device managed by a driver results in the invocation of the open() entry point.

Note that the open() entry point is not reentrant, though it is preemptive. Only one user task can execute the entry point code at a time for a particular device. Therefore, synchronization between tasks is not necessary in this entry point.

devno contains the major and minor device numbers (devno is the same as f>dev). To extract the major and minor device numbers from devno, use the major() and minor() macros, respectively. Refer to man pages for more information on macros major() and minor().

The file pointer f is defined in <file.h>. The open() entry point must return either OK or SYSERR.

open() Example

#include <file.h>

int dev_open(statics *s, int devno, file *f)
{
int minDevNo, majDevNo;

minDevNo = minor(devno);
majDevNo = major(devno);
/* perform initializing specific to minor device */
return (OK);
}

close()

The close() entry point function is called when the last open file descriptor pointing to a minor device is closed.

Specific allocation of memory done in the open() entry point routine is deallocated in the close entry point. As with the open() entry point, the close() entry point is not reentrant.

The prototype for close() is:

int close(statics *s, file *f)

where:

s Is a pointer to a statics data structure
f Is a pointer to a file structure

The file pointer f is defined in <file.h>. The close() entry point must return either OK or SYSERR.

close() Example

#include <file.h>

int dev_close(statics *s, file *f)
{

/* perform de-initializing specific to minor device */
return (OK);
}

read()

The read() entry point function is invoked in response to a read() system call. This entry point function is required to copy a specified amount of data from the device into a buffer designated by the calling routine.

The prototype for read() is:

int read(statics *s, file *f, char *buf, int count)

where:

s Is a pointer to a statics data structure
f Is a pointer to a file structure
buf Is a pointer to a character buffer
count Specifies the number of bytes to copy

The file pointer f is defined in <file.h>. The read() entry point routine attempts to copy from the input device count bytes of data into character buffer buf. If fewer bytes of data are copied than requested, read() returns the number of bytes actually copied, including zero, if appropriate. If any errors occur, SYSERR is returned.

read() Example

#include <file.h>

int dev_read(statics *s, file *f, char *buf, int count)
{
int byteCt;

/* perform copy operation */
return (byteCt);
}

write()

The write() entry point function is invoked in response to a write() system call. This entry point function is required to copy a specified amount of data from a buffer designated by the calling routine to the output device.

The prototype for write() is:

int write(statics *s, file *f, char *buf, int count)

where:

s Is a pointer to a statics data structure
f Is a pointer to a file structure
buf Is a pointer to a character buffer
count Specifies the number of bytes to copy

The file pointer f is defined in <file.h>. The write() entry point routine attempts to copy to the output device count bytes of data from character buffer buf. If fewer bytes of data are copied than requested, write() returns the number of bytes actually copied, including zero, if appropriate. If any errors occur, SYSERR is returned.

write() Example

#include <file.h>

int dev_write(statics *s, file *f, char *buf, int count)
{
int byteCt;

/* perform copy operation */
return (byteCt);
}

ioctl()

The ioctl() entry point function is called when the ioctl() system call is invoked for a particular device. This entry point is used to set certain parameters in the device or obtain information about the state of the device.

For character type device drivers, the prototype for ioctl() is:

int ioctl(statics *s, file *f, int command, char *arg)

where:

s Is a pointer to a statics data structure
f Is a pointer to a file structure
command Specifies the command to execute
arg Is a pointer to a command argument

For block type device drivers, the prototype for ioctl() is:

int ioctl(statics *s, int devno, int command, char *arg)

where:

s Is a pointer to a statics data structure
devno Indicates a device number
command Specifies the command to execute
arg Is a pointer to a command argument

The file pointer f is defined in <file.h>.

The driver defines the meaning of command and arg except for FIOPRIO and FIOASYNC, which are predefined and used by LynxOS to communicate with the drivers. If the arg field is to be used as a memory pointer, it should first be checked for validity with either rbounds() or wbounds(). (See "Validating Addresses".)

The kernel uses FIOPRIO to signal the change of a task priority to the driver that is doing priority tracking. FIOASYNC is invoked when a task invokes the fcntl() system call on an open file, setting or changing the value of the FNDELAY or FASYNC flag.

The kernel might change the priority of an I/O task in the case of priority inheritance to elevate the priority of a task that has locked a resource that another higher priority task is blocked on (see "Kernel Threads and Priority Tracking").

The ioctl() entry point returns OK or SYSERR.

ioctl() Example

Character type device driver:

int dev_ioctl(statics *s, file *f, int command, char *arg)
{
/* depending on the command, copy relevant
information to or from the arg structure */
}

Block type device driver:

int dev_ioctl(statics *s, int devno, int command, char *arg)
{
/* depending on the command copy relevant
information to or from the arg structure */
}

select()

The select() entry point function supports I/O polling or multiplexing.

The prototype for select() is:

int select(statics *s, file *f, int which, sel *ffs)

where:

s Is a pointer to a statics data structure
f Is a pointer to a file structure
which Specifies the condition to monitor
ffs Is a pointer to a sel data structure

The file pointer f is defined in <file.h>.

The which parameter is either SREAD, SWRITE, or SEXCEPT, indicating that the select() entry point is monitoring a read, write, or exception condition respectively. The select() entry point returns OK or SYSERR.

The following fields are required in the statics structure to support the select() system call:

struct statics
{
...
int *rsel_sem; /* sem for select read */
int *wsel_sem; /* sem for select write */
int *esel_sem; /* sem for select exception */
int n_spacefree; /* space available for write */
int n_data; /* data available for read */
int error; /* error condition */
};

The iosem field in the sel structure is a pointer to a flag that indicates whether the condition being polled by the user task is true or not. The sel_sem field is a pointer to a semaphore that the driver signals at the appropriate time (see below). The value of the semaphore itself is managed by the kernel and should never be modified by the driver. A driver must always set the iosem and sel_sem fields in the select() entry point.

A driver that supports select must also test and signal the select semaphores at the appropriate points in the driver, usually the interrupt handler or kernel thread. This should be done when data becomes available for reading, when space is available for writing or when an error condition is detected. For example:

/* data input */
s->n_data++;
disable (ps);
if (s->rsel_sem)
ssignal (s->rsel_sem);
restore (ps);

/* data output */
s->n_spacefree++;
disable (ps);
if (s->wsel_sem)
ssignal (s->wsel_sem);
restore (ps);

/* errors, exceptions */
if (error_found)
{
s->error++;
disable (ps);
if (s->esel_sem)
ssignal (s->esel_sem);
restore (ps);
}

select() Example

int dev_select (statics *s, file *f, int which, sel *ffs)
{
switch (which)
{
case SREAD:
ffs->iosem = &s->n_data;
ffs->sel_sem = &s->rsel_sem;
break;
case SWRITE:
ffs->iosem = &s->n_spacefree;
ffs->sel_sem = &s->wsel_sem;
break;
case SEXCEPT:
ffs->iosem = &s->error;
ffs->sel_sem = &s->esel_sem;
break;
}
return (OK);
}

strategy()

The strategy() entry point function is used only in block device drivers. It is used in place of the read() and write() entry point functions, which occur only in character device drivers.

When a process attempts to read or write from a file, the file system composes a linked list of data structures. Each data structure is of data type struct buf_entry, and describes the operation required for a single logical block of data. The entire linked list of these structures defines all the actions required of the device driver to complete the read or write operation.

The prototype for strategy() is:

int strategy(char *s, buf_entry *bp)

where:

s Is a pointer to a statics data structure
bp Is a pointer to the first structure in a linked list of buf_entry structures

The interface between the LynxOS file system and the device driver's strategy function is illustrated in the following diagram.

LynxOS File System and strategy() Interface

When the application uses a read() or write() system call, the file system module identifies which logical blocks of the file need to be read or written. It composes a linked list of buf_entry structures, each one describing the processing to be done for each logical block.

Memory blocks in the file system cache are set aside for the read or write. For a write, the data to be written into the disk block is copied to the cache location. For a read, the cache location is empty and receives the block of data when it is fetched from the disk. In each buf_entry structure, the pointer named memblock points to the corresponding cache block.

The linked list of buf_entry structures is used as the second argument to the strategy() entry point function. The strategy() entry point is responsible for causing the data to be moved between the cache and the disk. Typically, the actual data transfer is not accomplished by the strategy() entry point function itself. Instead, the buf_entry structures are passed to other functions that perform the transfer.

The buf_entry structure is defined in /usr/include/disk.h. It is defined as follows:

struct buf_entry {
int b_device;                 /* major/minor */
char *memblk;                 /* src or dest */
long b_number;               /* block number */
int b_status;                 /* op and status */
int b_rwsem;                  /* done sem */
int b_error;                  /* error flag */
struct buf_entry *av_forw;    /* next */
struct buf_entry *av_back;    /* avail */
int w_count;                  /* avail */
};

where:

b_device Encoded major and minor device number
memblk Memory source or destination returned by mmchain()
b_number Source or destination block on the device
b_status Specifies the type of transfer to perform, and the status of the transfer - The device is read if B_READ is set, or written to if B_READ is not set. To specify the status of the transfer, set B_DONE prior to signaling b_rwsem semaphore if the transfer is successful.
b_rwsem Semaphore to ssignal() when the transfer is complete
b_error Set non-negative if the transfer fails
av_forw Pointer to the next buffer to transfer, or NULL if end of list (may be changed by driver)
av_black, w_count Fields available to driver for its own purposes (LynxOS drivers use w_count to store the priority for priority tracking.)

Note: Set B_ERROR in addition to the b_error field if the transfer fails.

mmap()

The mmap() entry point is used to access memory-mapped character devices. The mmap() entry point is called as a result of the mmap(2) system call, allowing a character device to be mapped to memory space.

The prototype of the mmap entry point is described below:

mmap(struct file *f, kadd_t st, size_t len,
     int prot, int flags, off_t off)

Where:

f a pointer to a file
st the kernel address
len the size of the memory space
prot the bit field that specifies the protection bits
flags used to set flags
off the offset of memory

The file pointer f is defined in <file.h>. The mmap() entry point must return either OK or SYSERR. The prot field can include:

PROT_READ Read access
PROT_WRITE Write access
PROT_EXEC Executable
PROT_USER Specific User
PROT_ALL Any User

mmap() Example

#include <rcsid.h>

/* memdrvr.c - memread */

#include <kernel.h>
#include <errno.h>
#include <sys/file.h>
#include <inode.h>
#include <memobj.h>

extern MemoryObject *Phys_mem_object;

int memopen(char *stats, int dev, struct file *f)
{
f->inode->i_memobj = Phys_mem_object;
return (OK);
}
int memclose(char *stats, struct file *f)
{
f->inode->i_memobj = NULL;
return (OK);
}

int memread(char *s, struct file *f, char *buff, int count)
{
int i;
char *seekp;

seekp = (char *)(unsigned int)f->position;    /* Don't need a  */                                                       /* long long to  */
                                                      /* access memory */
if (recoset()) {
noreco();
pseterr(EFAULT);
return SYSERR;
}
for (i = 0; i < count; i++) {
buff[i] = *seekp++;          /* If a fault occurrs seekp */
                                             /* is the fault address */
        }
noreco();
f->position = (long long)((unsigned int)seekp);
return i;
}

int memwrite(char *s, struct file *f, char *buff, int count)
{
int i;
char *seekp;

seekp = (char *)(unsigned int)f->position;

if (recoset()) {
noreco();
pseterr(EFAULT);
return SYSERR;
}
for (i = 0; i < count; i++) {
               *seekp++ = buff[i];
}
noreco();
f->position = (long long)((unsigned int)seekp);
return i;
}

kaddr_t memmmap(struct file *f, kaddr_t st, size_t len,
     int prot, int flags, off_t off)
{
/* pass through the request to the real mapping function */
return phys_memory_mmap(f, st, len, prot, flags, off);
}


LynuxWorks, Inc.
855 Branham Lane East
San Jose, CA 95138
http://www.lynuxworks.com
1.800.255.5969
TOC PREV NEXT INDEX