Be Driven
  Device Drivers in the Be Os
     
    Serial Port Device Drivers

To be reworked into a more condensed article.


Volume III, Issue 27; July 7, 1999

BE ENGINEERING INSIGHTS: Device Drivers
By Rico Tudor rico@be.com

This article describes the structure of BeOS device drivers, with sample code showing the more popular kernel services. The PC serial port serves quite well as an example: ubiquitous, interrupt-capable, straightforward. And should you adopt the sample "qq" driver code as a testbed, there is no danger that your bugs will reformat the hard drive!

A driver is a kind of module, and can be loaded and unloaded from memory on demand. Unlike other types of modules, it can be accessed directly by user programs. To be reachable, a driver must "publish" a name in /dev-- this is done when the system boots. Once loaded, it has access to all hardware. Memory mapping is in effect in kernel mode, so the driver's access to memory is limited to kernel data and to user data of the current thread.

In keeping with BeOS practice, the driver operates in a multi- processing and multi-threaded environment. For example, while an interrupt is being serviced by CPU 0, background processing by CPU 1 may be underway for the same device. Attention to detail is recommended.

The first step is supplying code that initializes the driver when it is loaded. "api_version" is used to notify the kernel loader that we are using the latest driver API. The "device" struct contains private info for operating the serial port.

#include    <Drivers.h>
#include    <ISA.h>
#include    <KernelExport.h>


int32   api_version     = B_CUR_DRIVER_API_VERSION;


struct device {
    char    *name;
    uint    ioport,
            irq,
            divisor,
            nopen;
    bool    exists;
    sem_id  ocsem,
            wbsem,
            wfsem;
    uchar   wbuf[100];
    uint    wcur,
            wmax;
};


#define NDEV    ((sizeof devices) / (sizeof *devices))
static struct device devices[] = {
    { "qq1", 0x03F8, 4, 6 },
    { "qq2", 0x02F8, 3, 6 },
    { "qq3", 0x03E8, 4, 6 },
    { "qq4", 0x02E8, 3, 6 },
};


static isa_module_info  *isa;


static status_t
    qq_open( const char *, uint32, void **),
    qq_close( void *),
    qq_free( void *),
    qq_read( void *, off_t, void *, size_t *),
    qq_write( void *, off_t, const void *, size_t *),
    qq_control( void *, uint32, void *, size_t);


status_t init_driver( )
{
    status_t s = get_module( B_ISA_MODULE_NAME,
                                    (module_info **) &isa);
    if (s == B_OK) {
        struct device *d;
        for (d=devices; d<devices+NDEV; ++d) {
            d->exists = TRUE;
            d->ocsem = create_sem( 1, "qqoc");
            d->wbsem = create_sem( 1, "qqwb");
            d->wfsem = create_sem( 0, "qqwf");
        }
    }
    return (s);
}


void uninit_driver( )
{
    struct device *d;
    for (d=devices; d<devices+NDEV; ++d) {
        delete_sem( d->ocsem);
        delete_sem( d->wbsem);
        delete_sem( d->wfsem);
    }
    put_module( B_ISA_MODULE_NAME);
}

The kernel loader looks for the "init_driver" and "uninit_driver" symbols, and will call them just after loading and just before unloading, respectively. This code is guaranteed to execute single-threaded, and is a good place to prepare your synchronizing method. This driver uses three semaphores for each device. Note the matching calls to load and unload the ISA bus module: This was discussed by Ficus in last week's article.

At load time, the system will query for device names by calling the "publish_devices()" function. If you cannot supply the names yet, this step can be deferred using a method shown later. Finally, "find_device()" is called to get the general entry points into your driver. Note that the semantic meanings of the entry points are defined by their position in the "device_hooks" structure, eg. the "open" function entry point is always the first element of the structure. You can choose whatever names you want for these functions.

const char **publish_devices( )
{
    const static char *list[NDEV+1];
    uint i;
    uint j = 0;
    for (i=0; i<NDEV; ++i)
        if (devices[i].exists)
            list[j++] = devices[i].name;
    list[j] = 0;
    return (list);
}


device_hooks *find_device( const char *dev)
{
    static device_hooks dh = {
        qq_open, qq_close, qq_free,
        qq_control, qq_read, qq_write
    };
    return (&dh);
}

To use a device, the application invokes the open() system call, which ultimately results in a call on the device open function ("qq_open()" in the code below). The driver, having published the device names, must now map them to an internal form. This internal form is combined with the "flags" arg, creating a "cookie" which will be used in subsequent I/O. The cookie is handed back to the kernel through the "v" arg. A device can be open for multiple clients; if not already open, an interrupt handler is installed and the hardware is activated.

/* PC serial port (16550A UART)
 */
#define THR 0
#define DLL 0
#define DLH 1
#define IER 1
#define FCR 2
#define IIR 2
#define LCR 3
#define MCR 4
#define LCR_8BIT        0x03
#define LCR_DLAB        (1 << 7)
#define IER_THRE        (1 << 1)
#define MCR_DTR         (1 << 0)
#define MCR_RTS         (1 << 1)
#define MCR_IRQ_ENABLE  (1 << 3)
#define have_int( iir)  (((iir)&0x01) == 0)
#define THRE_int( iir)  (((iir)&0x0F) == 0x02)


struct client {
    uint            flags;
    struct device   *d;
};


static int32    qq_int( void *);
static void     setmode( struct device *);
void            *malloc( );


static status_t
qq_open( const char *name, uint32 flags, void **v)
{
    struct client *c;
    struct device *d = devices;
    while (strcmp( name, d->name) != 0)
        if (++d == devices+NDEV)
            return (B_ERROR);
    c = malloc( sizeof *c);
    if (c == 0)
        return (ENOMEM);
    *v = c;
    c->flags = flags;
    c->d = d;
    acquire_sem( d->ocsem);
    if (d->nopen == 0) {
        setmode( d);
        install_io_interrupt_handler( d->irq, qq_int, d, 0);
        (*isa->write_io_8)( d->ioport+FCR, 0);
        (*isa->write_io_8)( d->ioport+MCR,
                            MCR_DTR|MCR_RTS|MCR_IRQ_ENABLE);
        (*isa->write_io_8)( d->ioport+IER, 0);
    }
    ++d->nopen;
    release_sem( d->ocsem);
    return (B_OK);
}

The application concludes device use with the "close" system call, or by exiting. This triggers the device close function which, for the last client, entails hardware shutdown and removal of the interrupt handler. The kernel is also aware when the last close occurs, and will commence driver unloading.

Note the use of the open/close semaphore. In a multi-processor machine, the device may be opening and closing simultaneously. "ocsem" prevents a potential desktop firework display. (Awww!)

Bear in mind that the driver is subject to visitation by multiple threads, even if you adopt a "single open" policy. This is because the file descriptors returned by the open() system call are inherited by child processes (created by the fork() system call).

The device free function simply frees the cookies, if you are using them.

static status_t
qq_close( void *v)
{
    struct client *c = v;
    struct device *d = c->d;
    acquire_sem( d->ocsem);
    if (--d->nopen == 0) {
        (*isa->write_io_8)( d->ioport+MCR, 0);
        (*isa->write_io_8)( d->ioport+IER, 0);
        remove_io_interrupt_handler( d->irq, qq_int, d);
    }
    release_sem( d->ocsem);
    return (B_OK);
}


static status_t
qq_free( void *v)
{
    free( v);
    return (B_OK);
}

For brevity, this "qq" device read simply returns EOF. It does demonstrate the use of the "flags" arg, as originally passed in qq_open(). The only flag currently is O_NONBLOCK: when clear, the driver may block the thread to wait for I/O completion. Otherwise, the driver must return immediately with a short byte count, or with the B_WOULD_BLOCK error. Formally, O_NONBLOCK applies to device read, write, open, and close.

static status_t
qq_read( void *v, off_t o, void *buf, size_t *nbyte)
{
    struct client *c = v;
    *nbyte = 0;
    return ((c->flags & O_NONBLOCK)? B_WOULD_BLOCK: B_OK);
}

Here is a table of permissible read returns:

    full read:      *byte = *byte;      return (B_OK);
    partial read:   *nbyte = *nbyte/2;  return (B_OK);
    null read:      *nbyte = 0;         return (B_OK);
    error:          *nbyte = 0;         return (B_OUCH);

When writing a device write routine, the first decision to make is: How is data to be fetched from user space? Schematically (the four lines below show possible data flow--they are NOT C++ code), the four possible flows are:

    u -> kb -> hw
    u -> kb -> ikb -> hw
    u -> ikb -> hw          // use lock_memory
    u -> hw                 // dma

"u" is user space, "kb" is a kernel buffer, "ikb" is a kernel buffer accessed concurrently with the interrupt handler, and "hw" is the hardware. DMA gets into many complications, and usually involves writing for a BeOS subsystem (e.g. SCSI CAM), so I'll ignore that class. The code below uses u->kb->hw.

Data is copied by the thread into the "wbuf" kernel buffer. No data-structure contention exists because the interrupt handler is quiescent (interrupts are disabled), and "wbsem" locks out other writers. Interrupts are enabled after the copying, and the write synchronizes on "wfsem". As a side feature, signals can terminate the write.

static status_t
qq_write( void *v, off_t o, const void *buf, size_t *nbyte)
{
    cpu_status cs;
    struct client *c = v;
    struct device *d = c->d;
    uint n = 0;
    while (n < *nbyte) {
        acquire_sem_etc( d->wbsem, 1, B_CAN_INTERRUPT, 0);
        if (has_signals_pending( 0)) {
            *nbyte = n;
            return (B_INTERRUPTED);
        }
        d->wcur = 0;
        d->wmax = min( *nbyte-n, sizeof( d->wbuf));
        memcpy( d->wbuf, (uchar *)buf+n, d->wmax);
        (*isa->write_io_8)( d->ioport+IER, IER_THRE);
        acquire_sem( d->wfsem);
        n += d->wmax;
        release_sem( d->wbsem);
    }
    return (B_OK);
}

Code to implement the flow u->kb->ikb->hw yields smoother data flow, but is harder to write. You must use a semaphore, spinlock and bool to synchronize with live interrupts while accessing "ikb".

For the efficiency fiend, only u->ikb->hw will do: include the above mechanisms, and lock_memory() as well. The latter ensures the user buffer is resident (not swapped to disk). Otherwise, while filling "ikb", your may page-fault with a spinlock set. This invites deadlocks and reset buttons. Watch for our exciting article on BeOS Synchronization, coming soon.

The permissible write returns are the same as those for read. However, avoid the null write, since it causes some apps to loop forever.

The interrupt handler, if you install one, is called with the argument of your choice: often it's the cookie, but "qq" passes the device struct. Since "qq" only does output, you can match this code with the device write routine. Briefly, the hardware is fed data until none remains: then interrupts are disabled and "wfsem" is used to unblock the background.

WARNING: the handler interrupts whatever thread is running, and the memory map varies correspondingly. Therefore, all accesses to user space by the handler are strictly forbidden. That is why the device write routine is committed to the user/kernel data shuffle.

Since multiple devices may share the same IRQ, return B_UNHANDLED_INTERRUPT if your hardware was not the interrupt cause. Otherwise, return B_HANDLED_INTERRUPT, or B_INVOKE_SCHEDULER for immediate scheduling.

static int32
qq_int( void *v)
{
    struct device *d = v;
    uint h = B_UNHANDLED_INTERRUPT;
    while (TRUE) {
        uint iir = (*isa->read_io_8)( d->ioport+IIR);
        if (have_int( iir) == 0)
            return (h);
        h = B_HANDLED_INTERRUPT;
        if (THRE_int( iir)) {
            if (d->wcur < d->wmax)
                (*isa->write_io_8)( d->ioport+THR,
                                        d->wbuf[d->wcur++]);
            else {
                (*isa->write_io_8)( d->ioport+IER, 0);
                release_sem_etc( d->wfsem, 1,
                                    B_DO_NOT_RESCHEDULE);
            }
        }
        else
            debugger( "we never make mistakes");
    }
}

Next, we have the catch-all function, device control. Unlike the other entrypoints, you need to range check the user addresses. This is achieved with is_valid_range(). The "com", "buf" and "len" args are yours to interpret, although BeOS defines some standard cases. One pair of commands, B_SET_NONBLOCKING_IO and B_SET_BLOCKING_IO, allows changing our friend O_NONBLOCK. Oddly, the relevant system call is fcntl().

For other commands, use the ioctl() system call to reach this routine. 'getd' and 'setd' allow the baud rate divisor (19200 baud by default) to be controlled by an app: set it to 1 to go really fast.

#define PROT_URD    0x00000004  /* user read */
#define PROT_UWR    0x00000008  /* user write */


static status_t
qq_control( void *v, uint32 com, void *buf, size_t len)
{
    struct client *c = v;
    struct device *d = c->d;
    switch (com) {
    case 'getd':
        if (is_valid_range( buf, sizeof( int), PROT_UWR)) {
            *(int *)buf = d->divisor;
            return (B_OK);
        }
        return (B_BAD_ADDRESS);
    case 'setd':
        if (is_valid_range( buf, sizeof( int), PROT_URD)) {
            d->divisor = *(int *)buf;
            setmode( d);
            return (B_OK);
        }
        return (B_BAD_ADDRESS);
    case B_SET_NONBLOCKING_IO:
        c->flags |= O_NONBLOCK;
        return (B_OK);
    case B_SET_BLOCKING_IO:
        c->flags &= ~ O_NONBLOCK;
        return (B_OK);
    }
    return (B_DEV_INVALID_IOCTL);
}


static void setmode( struct device *d)
{
    uint div = d->divisor;
    (*isa->write_io_8)( d->ioport+LCR, LCR_DLAB);
    (*isa->write_io_8)( d->ioport+DLL, div & 0x00ff);
    (*isa->write_io_8)( d->ioport+DLH, div >> 8);
    (*isa->write_io_8)( d->ioport+LCR, LCR_8BIT);
}

To republish your device names, execute the following code fragment:

    int fd = open( "/dev", O_WRONLY);
    write( fd, "qq", strlen( "qq"));
    close( fd);

This will work in your driver, or in an app. This is a handy feature for hot-swapping, or decommissioning faulty equipment.

Finally, to bring your driver creation to life, you need to compile and install. Here are the commands for the "qq" driver:

    gcc -O -c qq.c
    gcc -o qq qq.o -nostdlib /system/kernel_intel
    mv qq /system/add-ons/kernel/drivers/bin/qq
    cd /system/add-ons/kernel/drivers/dev
    ln -s ../bin/qq

After republishing or rebooting, your devices will appear in /dev.

 


The Communal Be Documentation Site
1999 - bedriven.miffy.org