Skip site navigation (1)Skip section navigation (2)
Date:      Mon, 24 Feb 1997 14:05:08 -0500
From:      Brian McGovern <bmcgover@cisco.com>
To:        hackers@freebsd.org
Subject:   Pseudo-driver cookbook (phase II)
Message-ID:  <199702241905.OAA00646@bmcgover-pc.cisco.com>

next in thread | raw e-mail | index | archive | help

Ok. So I'm getting a little bored, and I made a big jump at the end ;) Sue me.

Anyhow, here is round two. Its probably (easily) twice as big as the 
original, and gets in to the topics I only mentioned in the first round.

I have corrected a few things that people mentioned (I probably missed
a lot more :) :) ). Also, I want to say up front that I discarded several
suggestions, just to keep the first part of the document "as completely
brain-numbing simple" as I could. Please don't take offense. I just
want a quick-to-understand document.

As always, suggestions are welcome. I'll probably make at least one or two
more passes over it before formally posting it on my web site, and
submitting it to the core team for inclusion someplace (handbook?).

I'll also probably start work on a character/tty driver, as soon as I
get a peice of hardware I'm expecting.

To all of those who made it possible to do this document, I thank you -
especially Michael Smith, who has managed to so far answer all of my
really stupid questions. There will be more. Anyhow, here goes....


	-Brian

[Ed Note: This document has been written to document what I have learned
over the past month about writing pseudo-device drivers for the FreeBSD
2.x and 3.0 operating systems. I realize that it is probably incomplete.
However, it is designed to give someone with no knowledge of device drivers
a step in the right direction. It is no substitute for a well
written manual on writing device drivers. I recommend that all
perspective driver-writters take the time to read at least one or two
books on the subject cover to cover before using this document to explore
the differences between what they've read about, and what FreeBSD uses.

I also expect that in the future, I will be doing a similar document
for a real character/tty device driver.

If you have any questions, you may email me. However, chances are, since I am
_just_ learning how to do this myself, I'll have to forward your email
to the hackers mailing list.		-BJM]


Writing a character pseudo-device driver is actually not all that difficult,
once you understand all of the steps that are required. Hopefully, this walk
through of creating a character pseudo-device driver will set the stage for
the understanding of writing other device drivers, with more functionality.

The first step is to think about what you want to accomplish with the device
driver, and how to go about putting it all together. In our first example,
we will create a very simplistic driver - one that will allow you to open
the device, close it, and read from it. When you read, it will return part,
or all of a specified message, based on the size of your reads.

For those of you who have absolutely no background on writing a device driver,
I recommend you head off to the nearest book store, and pick up a book or
two on the subject. Although the details of "how" to write one for a particular
system may not apply, the background and concepts will. I will attempt to
replicate the needed information here, but I can not guarentee to cover
everything that may play a role in the device driver you wish to write.

The first thing you'll need to do is set up a working enviornment in the kernel
source tree. I recommend creating a directory (for pseudo-devices) under
/usr/src/sys/dev/YOUR_DRIVER_NAME. I'll be calling my driver "foo", so I'll
create the directory /usr/src/sys/dev/foo.

Now, within this directory, I'm going to create a file called foo.c. I use the
name "foo", simply so I know it will relate to my "foo" driver. Make sense?
I hope so :)

In this file, I'm going to include several headers, right off the bat,
so my source will look like:

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/kernel.h>

These include files will include everything that we need for our simple

device driver (and quite possibly more).

Now, the next logical thing to do would be to define a MAJOR NUMBER for
our device driver. A major number is merely a unique number to seperate
our device driver from all of the other device drivers. You can find the
used major numbers in /usr/src/sys/i386/conf/majors.i386. Since our
device driver will be character based, we look for the character major numbers,
and then look for an unused one. In our case, number 20 is "reserved for local
use", so we'll use that number, since our device driver will never be
distributed, and therefore, should never conflict (unless we write another
character device driver locally). What we will do now is #define CDEV_MAJOR
to be equal to the number we just chose. This is one of those "standards" that
just make sense. You'll find that doing it this way, rather than just
hard-coding the major number, makes it easier to change later, as well as
lets other developers know what to look for, in case they ever have to work
on your driver. It'll also help stop typos, as well.

So, now, your driver code should look like:

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/kernel.h>

#define CDEV_MAJOR 20

Not bad. We're making some headway.

The next thing that I want to do is define the buffer that we'll be using
for our driver. In my case, I want a character buffer with a string of
my choice in it (my message). Hence, I'll add one more line to our code,
making it look like:

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/kernel.h>

#define CDEV_MAJOR 20

static char message[] = "Wow. I really have a device driver here!\n\0";


You'll notice that the string is declared static. This is so that it will
not conflict with any other variables called "message" elsewhere in the
kernel. You will see, rather quickly, that nearly all global variables,
or exported functions, are declared static.

Now, we only have a few more things to set up for our device driver. Since
we'll only be concerned about reading from our driver, we'll only concern
ourselves explicitly with the read routine. We will "fake" the open
and close routines for the time being, so that they will always succeed. Note,
that this can cause problems, simply because they'll be able to open
dozens of "foo" devices. However, we'll add some error checking to our first
read routine to keep from selecting invalid minor numbers. Note, once we
have a working open and close routine, we'll be able to move the error
checking there, and out of the read routine completely. Having error checking
in the open routine is the "proper" thing to do. However,
I've placed it in the read routine (and left the open routine NULL) in order
to create the most simplistic device driver, and still handle the error
cases. In all future drivers, error checking will be in the open routine.
Note that the kernel does a certain amount of pre-error checking, and will
not allow the program to call driver routines with improper (not open)
minor numbers.

Now, you may ask "What is a minor number?". Minor numbers are used to
distinguish subsets of a device from one another. Although the interpretation
of the minor device number is left entirely to the descretion of the
driver itself, it is usually used to distinguish between physical devices.
For instance, all 8250- and 16550- type serial devices will share a major
number of 28, while each individual serial port will have its own minor
number, or, in the case of the SIO driver, it will have multiple - one
for dial in, dial in initial state, dial in lock state, dial out, dial out
initial state, and dial out lock state. The point is, this is how you
keep track of which specific device needs to be handled.

Ok. The next step will be to define some information about our read routine.
Again, this is something that "just has to be done", so we'll do it. Basically,
it sets up our to-be-defined read routine as an appropriate type of
function to be a device-driver read routine. So, now, make your driver
code look as follows (I've added comments for clarity at this point):


/* Include the header files that we'll need */
#include <sys/param.h> 	
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/kernel.h>

/* Define our MAJOR NUMBER for the device driver */
#define CDEV_MAJOR 20

/* Define our message buffer, with a prebuilt message */
static char message[] = "Wow. I really have a device driver here!\n\0";

/* Define our routines in the proper format for a device-driver */
static d_read_t fooread;


Now, you'll notice with that last line that we declared something called
"fooread" as being of some type "d_read_t". "d_read_t" is the type of 
function that the kernel expects as a device driver read routine. fooread
will be the name of our function. Note it is declared static (See above).

One note on device drivers I've failed to touch on that suddenly has come
to me. Device driver "names", in our case, "foo", can be anywhere from 1-4
characters. Most systems do not allow numbers (for reasons that will
become obvious later). Once defined, all references to that device driver
will use that name.

Anyhow, to get back on track... The next thing we have to do is set up a block
that will define our device driver for the kernel. It is basically a structure
that contains pointers to all of our routines that comprise the device driver,
plus a few other flags (that I won't get in to here). Note the use of null
and nx before the names of the routines. Using null will always make that
call "succeed" to your application (although nothing will actually get done).
using nx will always make that call "fail" to your application (again, with
nothing really getting done).

So, now we'll make our code look like this:

/* Include the header files that we'll need */
#include <sys/param.h> 	
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/kernel.h>

/* Define our MAJOR NUMBER for the device driver */
#define CDEV_MAJOR 20

/* Define our message buffer, with a prebuilt message */
static char message[] = "Wow. I really have a device driver here!\n\0";

/* Define our routines in the proper format for a device-driver */
static d_read_t fooread;

/* Define the cdewsw structure for our driver */
static struct cdevsw foo_cdevsw =
  {
    nullopen, nullclose, fooread, nxwrite,
    nxioctl, nxstop, nxreset, nxdevtotty,
    nxselect, nxmmap, NULL, "foo", NULL, -1
  };

You'll note we set the open and close routines to nullopen and nullclose,
respectively. This will allow them to always succeed, because we will not
be able to read from a device until it is "opened", and most programs will
try to close it once its done with it. Therefore, these two calls should
always succeed. You'll then note that we have fooread declared as the
read routine. Again, this is the function we'll write to actually handle
reading from our pseudo-foo device. All other functions have been set
nx..., which means that calls to these routines (such as write(), select(),
ioctl(), mmap(), etc) will all fail.

Itching to go yet? We're almost there. Two functions, and a couple of
declarations, and we'll be done. Lets look at the read routine itself now.

The read routine is passed three parameters. A dev_t structure which gives
device information, a pointer to a struct uio, which defines the transfer to
occur, and an integer flag. Since we're keeping things simple, I won't talk
about flags right away. The dev_t structure, and the uio structure, however,
are critical to our application.

The most useful (to our cause) portion of the dev_t structure will be the
minor device number. We'll need this to verify whether we have a valid device.
Also, as we move forward, and rewrite the device driver to handle more
than one pseudo-device, we'll use the minor number to make decisions as to
what buffer we'll actually manipulate.

The uio structure is a little more complicated. For now, we'll do very little
with it, other than use it to work out our read request. However, we will
eventually use two of the structures members, so I'll note them now.

The first is uio_resid. This is the number of bytes that the application
desires to read. We do not have to actually read this much, but its useful
in defining how much data we have to move.

The second is uio_offset. This is the offset in to the data stream that the
read is to take place. In our first pass, we will not use this field at all,
as we'll always return reads starting from offset 0 (for the sake of ease).
Later, we'll code so that the buffer appears to be a "ring" buffer, repeating
the message until the read request is satisfied. Future reads on the same
descriptor will also start back up where the prior read left off.

You'll note that the function uiomove forms the base of our read routine.
Basically, uimove is called with a pointer to the in-kernel data buffer,
the number of bytes to move, and the pointer to the uio structure passed to
the routine.

So, now, to define our read routine, we'll do something like this (note: I
will no longer display the full source file again until we are 100% done,
in order to save space).

static int fooread(dev_t dev, struct uio *myuio, int flag)
  {
    int result;		 /* The result of the uiomove function */
    if (minor(dev) != 0) /* If someone tries to read from other than minor # */
      return ENODEV;	 /* 0, reject the request saying there is no device */

    result = uiomove(message, MIN(myuio->uio_resid, sizeof(message)), myuio);

    return result;
  }

You'll notice that I used the MIN of either the amount the user program wanted
to read, or the size of our message. This will make both small reads work,
as well as returning short reads if they ask for a larger chunk of data.

Now, we're 80% there. The last thing we need to do is actually write the code
to set up the device, and get us up and running. Once these last two peices
are done, we can actually compile the routine, and try it out.

The next routine we'll write is the init routine, which is responsible for
"starting up" our device driver. It can be as simple as stating the driver
is loaded, or it can be very complex. In our case, since we're doing a
pseudo-device driver, and not a regular character device driver, we won't
have probe and attach routines to do the startup work, so we'll have to do
it ourselves in the init routine. Our init routine will look something like:

static void fooinit(void *unused)
  {
    dev_t dev;
    printf("foo0: Foo test character device driver installed and ready.\n");
    printf("foo0: Configured for 1 device\n");
    dev = makedev(CDEV_MAJOR,0);
    cdevsw_add(&dev, &foo_cdevsw, NULL);
  }

This routine basically will print out two lines of notification on boot up,
and then get the device set up in the kernel.

The last thing we need to do is tell the kernel (from inside the driver) what
routine has to be executed in order to get everything started, as well as give
it a "priority" for loading. As an example of "priority", you can look
at the Ethernet cards defined in the LINT kernel config file. You'll note
it says not to change the order, in order to keep them from
screwing up each other's probes... This is similar. Unless you have a very
good reason to change it, I recommend to use the default priority. With this
in mind, the following default line should work 99% of the time:

SYSINIT(foodev, SI_SUB_DRIVERS, SI_ORDER_MIDDLE+CDEV_MAJOR, &fooinit, NULL);

What does it all do? Even I don't know ;) But, it works, so use it.

Now, all thats left is some touchup. Obviously, we won't want to have our
driver load in to a kernel that doesn't have it configured in. When we run
the config utility later on, it will generate a .h file in the compilation
directory, with a file name thats the same as our device driver name.
In that header file will be a single declaration, that will consist of
a #define N*, where * is replaced with an all upper-case name of our
driver. In our case, it will be a #DEFINE NFOO n, where n is replaced
by the number of pseudo devices we configured. In this case, it should
never be more than one, but it could be. Therefore, we'll make our whole
driver look like this:

#include "foo.h"
#if NFOO > 0
/* Include the header files that we'll need */
#include <sys/param.h> 	
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/kernel.h>

/* Define our MAJOR NUMBER for the device driver */
#define CDEV_MAJOR 20

/* Define our message buffer, with a prebuilt message */
static char message[] = "Wow. I really have a device driver here!\n\0";

/* Define our routines in the proper format for a device-driver */
static d_read_t fooread;

/* Define the cdewsw structure for our driver */
static struct cdevsw foo_cdevsw =
  {
    nullopen, nullclose, fooread, nxwrite,
    nxioctl, nxstop, nxreset, nxdevtotty,
    nxselect, nxmmap, NULL, "foo", NULL, -1
  };

static int fooread(dev_t dev, struct uio *myuio, int flag)
  {
    int result;		 /* The result of the uiomove function */
    if (minor(dev) != 0) /* If someone tries to read from other than minor # */
      return ENODEV;	 /* 0, reject the request saying there is no device */

    /* Now do the real read */
    result = uiomove(message, MIN(myuio->uio_resid, sizeof(message)), myuio);

    return result;
  }

static void fooinit(void *unused)
  {
    dev_t dev; /* The device we'll create */
/* Now, put some output to the console, so we know we're here */
    printf("foo0: Foo test character device driver installed and ready.\n");
    printf("foo0: Configured for 1 device\n");
/* Actually create the device */
    dev = makedev(CDEV_MAJOR,0);
    cdevsw_add(&dev, &foo_cdevsw, NULL);
/* All done! */    
  }

/* Tell the kernel how to get us started */
SYSINIT(foodev, SI_SUB_DRIVERS, SI_ORDER_MIDDLE+CDEV_MAJOR, &fooinit, NULL);

#endif


Now, thats it for the driver. The next step is to go in to
/usr/src/sys/i386/conf. I modified majors.i386 so that character device
20 was called "foo". This was not required, but if I had other developers
working on this system, I'd want them to know I used 20 for my device
driver, and its no longer available for them to use until I'm done with it,
and change it back.

Secondly, you'll need to edit files.i386. Add the line

dev/foo/foo.c		optional	foo	device-driver

someplace in the file. I usually do it about 25-30 lines down,
where the similar looking lines start. Placement in the file is not critical,
I just like to keep things looking consistent.

Almost there. Just a couple of more things. Next, create your kernel config
file (getting real close now). Add the line:

pseudo-device foo 1

someplace to the file. This will let the kernel know you need the foo driver,
and it will tell the driver it wants one device (more on this later when
we support multiple devices).

Now config the kernel, make it as per usual, and install it as per usual. If
there are errors or problems, check your source again for typos, unterminated
comments, etc. Once you've got it compiled and installed, continue.

The last thing to do is to actually create the device in the /dev
subdirectory. So, cd to it now. The command for making a device node is
called mknod. Its standard format (according to itself when run with no
parameters) is: mknod name [b | c] major minor.

For our device driver, we'll use something like:

mknod foo0 c 20 0

Which tells it to create a device called foo0, which will be a character
device with a major number of 20, and a minor number of 0. Guess what. We're
done. Reboot the box.

You should be able to read from the device now (assuming you saw the banner
lines on startup) with cat < /dev/foo0. Your "message" will
repeat infinately, until you hit CTRL-C. Also, you should be able to make
open() calls to /dev/foo0, and read them with the read() call, as per a normal
device. Note, however, that each read() will return the message from the
beginning, and if your buffer is too small, you'll never see the end of it.
This is also true with the cat case if you make your message too long. However,
I know that cat uses a rather large buffer, so this will be difficult to do.

Now, we'll create a slightly more full-fledged driver. We'll add open
and close routines, and move the minor number error checking to the open
routine, where it really belongs.

The close routine will be the most straight forward, so we'll do that
first. Since close will never be called unless a successful open has
occured, and we don't have any real work to do to close the device,
the function itself will always just return a success. Heres how it will
look. Note, that we could have left the close routine out, and leave
the cdevsw structure set to nullclose. However, I've included it to
show you how to stub it, so you can add your own features later.

static int fooclose(dev_t dev, int flags, int mode struct proc *p)
  {
    return 0;
  }


The open routine will be almost as straight forward. All that it has to do
is check the minor device number, to see if its valid. If it is, it will
return 0 (a success). Otherwise, we'll have it return ENODEV, stating
to the caller that there is no such device.

static int fooopen(dev_t dev, int flags, int mode struct proc *p)
  {
    if (minor(dev) != 0)
      return ENODEV;
    else
      return 0;
  }

Pretty easy. Now, lets glue them in. We'll need to add the routines to our
cdevsw structure, as well as declare them properly before the cdevsw structure.
Here, again, I've layed the whole device driver out, so you can see what
it should look like:


#include "foo.h"
#if NFOO > 0
/* Include the header files that we'll need */
#include <sys/param.h> 	
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/kernel.h>

/* Define our MAJOR NUMBER for the device driver */
#define CDEV_MAJOR 20

/* Define our message buffer, with a prebuilt message */
static char message[] = "Wow. I really have a device driver here!\n\0";

/* Define our routines in the proper format for a device-driver */
static d_read_t fooread;
static d_open_t	fooopen; 	/* WE JUST ADDED THESE ! */
static d_close_t fooclose;


/* Define the cdewsw structure for our driver */
static struct cdevsw foo_cdevsw =
  {
    fooopen, fooclose, fooread, nxwrite,/* Note: changed nullopen and nullclose */
    nxioctl, nxstop, nxreset, nxdevtotty, /* to fooopen and fooclose */
    nxselect, nxmmap, NULL, "foo", NULL, -1
  };

/* New open routine */
static int fooopen(dev_t dev, int flags, int mode struct proc *p)
  {
    if (minor(dev) != 0)
      return ENODEV;
    else
      return 0;
  }

/* New close routine */
static int fooclose(dev_t dev, int flags, int mode struct proc *p)
  {
    return 0;
  }


/* "Corrected" read routine, to put error checking where it belongs (in open) */
static int fooread(dev_t dev, struct uio *myuio, int flag)
  {
    int result;		 /* The result of the uiomove function */
			 /* Note the error checking is now gone! See foopen */
    /* Now do the real read */
    result = uiomove(message, MIN(myuio->uio_resid, sizeof(message)), myuio);

    return result;
  }

static void fooinit(void *unused)
  {
    dev_t dev; /* The device we'll create */
/* Now, put some output to the console, so we know we're here */
    printf("foo0: Foo test character device driver installed and ready.\n");
    printf("foo0: Configured for 1 device\n");
/* Actually create the device */
    dev = makedev(CDEV_MAJOR,0);
    cdevsw_add(&dev, &foo_cdevsw, NULL);
/* All done! */    
  }

/* Tell the kernel how to get us started */
SYSINIT(foodev, SI_SUB_DRIVERS, SI_ORDER_MIDDLE+CDEV_MAJOR, &fooinit, NULL);

#endif


There. That wasn't so painful. Now that we have a reasonably functioning driver,
lets add some flexability. Lets now say that we wanted to implement our message
buffer as a ring buffer, so that each read would pick up where the last one
left off, and that if a read was big enough to go off the end of the buffer,
we'd wrap instead of returning a short read. 

Since we're changing the operation of how we'll be reading data, we'll obviously
be looking at the read routine. Note how we begin to add some complexity.

static int fooread(dev_t dev, struct uio *myuio, int flag)
  {
    int toread, result, offset;

    while (myuio->uio_resid) /* Number of characters still in the request
				pipe */
      {
        offset = myuio->uio_offset % sizeof(message);
        toread = min(myuio->uio->resid, sizeof(message));
	result = uiomove(message + offset, toread, myuio);
	if (result)
	  return result;
      }
    return 0;
  }

These changes allow the buffer to be circular. As mentioned earlier, the
uio_resid member of the uio structure maintains the number of bytes
to be read. The uio_offset member keeps track of where in the data stream
you are. Note that there are no changes to these members themselves. They're
only being referenced in the code, not set. This is because the function
uiomove handles all of the required housekeeping on these values.

Now, lets tackle writing to the message itself. This will require one
additional routine for writing, and some changing of how the buffer is
manipulated.

Lets look at the write routine first. In this example, I'm always going
to have the write routine start at the beginning of the buffer, and
truncate messages that are too long to fit. It does not have to be this
way. However, I'd like to make it a little less confusing, as if I concerned
myself with offsets, and allowed writes to "ring" around, its possible I can
delete the first portion of my message. Additionally, when a new reader
opens the device, I want them to start at the beginning of my message,
not part way through. If you'd like to enhance this as an exercise for
yourself, feel free. Thats exactly the type of experimentation that will
help you learn how these things work.

Here is a simple write routine for us to start with. You'll note that
there is a define FOOBUFSIZE that we haven't talked about yet. I'll
speak more about it after I show you the routine, so don't worry.

static int foowrite(dev_t dev, struct uio *myuio, int flag)
  {
    int towrite, result;

    towrite = min(myuio->uio_resid, (FOOBUFSIZE - 1));
    uiomove(message, towrite, myuio);
    message[towrite] = 0;
    return 0;
  }

Now, we'll also have to change how we declare our message buffer. Currently,
its stored in the kernel as a string constant, which, although you COULD
change it, it defeats the concept of "constant". Therefore, I'd like
to redefine it to a more familar character string. So, we'll change
the declaration to:

unsigned char message[FOOBUFSIZE];

Now, what is this FOOBUFSIZE? Its something we'll have to define to
establish the size of the buffer we'll be using. What I usually do is something
like this:

#ifndef FOOBUFSIZE
#define FOOBUFSIZE 1024
#endif

This way, if it isn't previously declared, the buffer will default to 1K.
This method allows you to specify a line that looks like the following in
your config file, and it will be used by the driver:

options	FOOBUFSIZE=2048

This will allow you to use 2K buffers. For sanity's sake, it'd be a good
idea to put it right before or after the "pseudo-device foo 1" line
in your config file. Or, if you're like me, and are anal about keeping
the various lines of your config files together (ie - all of your
options together, all of your devices, etc), you may want to comment what
its for.


You'll notice in the code sample below that I also changed the sizeof(message)
calls in fooread to using strlen(message). This is keep everything working
if you write a short message to your buffer - it will wrap at the first
0 byte it sees, rather than at the end of the message space itself. Note,
you can no longer use this driver for binary information. However, it will
still work fine with text.

The last thing you need to do is add the write routine to your cdevsw
structure, and define it just before (as we did with read, open, and close).



So, now we should look like this:



#include "foo.h"
#if NFOO > 0
/* Include the header files that we'll need */
#include <sys/param.h> 	
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/kernel.h>

/* Define our MAJOR NUMBER for the device driver */
#define CDEV_MAJOR 20

/* Define our message buffer, with a prebuilt message */
static unsigned char message[FOOBUFSIZE] = "Wow. I really have a device driver here!\n\0";

/* Define our routines in the proper format for a device-driver */
static d_read_t fooread;
static d_write_t foowrite;	/* Add me here! */
static d_open_t	fooopen; 
static d_close_t fooclose;


/* Define the cdewsw structure for our driver */

/* Note we now have foowrite, as well */
static struct cdevsw foo_cdevsw =
  {
    fooopen, fooclose, fooread, foowrite,  
    nxioctl, nxstop, nxreset, nxdevtotty, 
    nxselect, nxmmap, NULL, "foo", NULL, -1
  };

/* New open routine */
static int fooopen(dev_t dev, int flags, int mode struct proc *p)
  {
    if (minor(dev) != 0)
      return ENODEV;
    else
      return 0;
  }

/* New close routine */
static int fooclose(dev_t dev, int flags, int mode struct proc *p)
  {
    return 0;
  }


/* Now fooread utilizes a circular queue scheme */
static int fooread(dev_t dev, struct uio *myuio, int flag)
  {
    int toread, result, offset;

    while (myuio->uio_resid) /* Number of characters still in the request
				pipe */
      {
        offset = myuio->uio_offset % strlen(message);
        toread = min(myuio->uio->resid, strlen(message));
	result = uiomove(message + offset, toread, myuio);
	if (result)
	  return result;
      }
    return 0;
  }

/* Foowrite writes to the string, and zeros the last character */
static int foowrite(dev_t dev, struct uio *myuio, int flag)
  {
    int towrite, result;

    towrite = min(myuio->uio_resid, (FOOBUFSIZE - 1));
    uiomove(message, towrite, myuio);
    message[towrite] = 0;
    return 0;
  }


static void fooinit(void *unused)
  {
    dev_t dev; /* The device we'll create */
/* Now, put some output to the console, so we know we're here */
    printf("foo0: Foo test character device driver installed and ready.\n");
    printf("foo0: Configured for 1 device\n");
/* Actually create the device */
    dev = makedev(CDEV_MAJOR,0);
    cdevsw_add(&dev, &foo_cdevsw, NULL);
/* All done! */    
  }

/* Tell the kernel how to get us started */
SYSINIT(foodev, SI_SUB_DRIVERS, SI_ORDER_MIDDLE+CDEV_MAJOR, &fooinit, NULL);

#endif




Thats about it for a basic device driver. There are some extras that you
can throw in, like supporting multiple buffers, select() and ioctl()
call support, etc, but those are all fairly straight forward, especially
with a pseudo-device driver (for instance, with select(), you should
ALWAYS be able to write). Therefore, I'm going to skip quite a bit,
and go to a finished driver that includes some of these features. You'll
be able to set your config line (pseudo-device foo X) to any positive
value of X you like. Also, you can set FOOBUFSIZE in your config file to
get buffers larger or smaller than 1K.

Here's the complete code. See what you can do with it.

[Ed Note: As of this writing, some code, such as the select routine and
ioctl call, has not been fully tested. It has been included merely
as an example. At some future time, the code should be tested and
debugged properly (although it "appears" to work now)].


#include "foo.h"
#if NFOO > 0

#include <sys/param.h> 	
#include <sys/systm.h>
#include <sys/ioctl.h>
#include <sys/conf.h>
#include <sys/uio.h>
#include <sys/kernel.h>


#define CDEV_MAJOR 20

#ifndef FOOBUFSIZE
#  define FOOBUFSIZE 1024
#endif

typedef struct { char buffer[FOOBUFSIZE];} msgbuf;

msgbuf messagebuf[NFOO];

struct foosoftc
  {
    int written;
  };

struct foosoftc fsc[NFOO];

static d_read_t fooread;
static d_write_t foowrite;
static d_open_t fooopen;
static d_close_t fooclose;
static d_select_t fooselect;
static d_ioctl_t fooioctl;

static struct cdevsw foo_cdevsw =
  {
    fooopen, fooclose, fooread, foowrite,
    fooioctl, nxstop, nxreset, nxdevtotty,
    fooselect, nxmmap, NULL, "foo", NULL, -1
  };
    

static void fooinit(void *unused)
  {
    dev_t dev;
    int i;
    printf("Test character driver\n");
    dev=makedev(CDEV_MAJOR,0);
    cdevsw_add(&dev, &foo_cdevsw, NULL);
    for (i=0;i<NFOO;i++)
      bzero(messagebuf[i].buffer,FOOBUFSIZE);
    for (i=0;i<NFOO;i++)
      fsc[i].written = 0;
  }


static int fooopen(dev_t dev, int flags, int mode, struct proc *p)
  {
    if (minor(dev) >= NFOO)
      return ENODEV;
    else
      return 0;
  }

static int fooclose(dev_t dev, int flags, int mode, struct proc *p)
  {
      return 0;
  }

static int fooselect(dev_t dev, int rw, struct proc *p)
  {
    switch (rw)
      {
        case FREAD:
	  {
	    if (fsc[minor(dev)] != 0)
 	      return 1;
	    else
	      return 0;
	    break;
	  }
	case FWRITE:
	  {
	    return 1;
	    break;
	  }
	case 0:
	  {
	    return 0;
	    break;
	  }
      }
    return 0;
  }
  
static int fooioctl(dev_t dev, int cmd, caddr_t data, int flags, struct proc *p)
  {
    switch(cmd)
      {
        case FIONREAD:
	  {
	    *data = strlen(messagebuf[minor(dev)].buffer);
	    break;
	  }
	default:
	  {
	    return ENOTTY;
	    break;
	  }
      }
    return 0;
  }

static int foowrite(dev_t dev, struct uio *myuio, int flag)
  {
    int towrite, result, offset;

    offset = myuio->uio_resid;
    towrite = min(myuio->uio_resid, (FOOBUFSIZE - 1));
    uiomove(messagebuf[minor(dev)].buffer, towrite, myuio);
    messagebuf[minor(dev)].buffer[offset] = 0;
    fsc[minor(dev)] = 1;
    return 0;
  }
    
static int fooread(dev_t dev,struct uio *myuio, int flag)
  {
    int toread, result, offset;

    if (strlen(messagebuf[minor(dev)].buffer) == 0)
      return EAGAIN;
    while(myuio->uio_resid)
      {
        offset = myuio->uio_offset % strlen(messagebuf[minor(dev)].buffer);
	toread = min(myuio->uio_resid, strlen(messagebuf[minor(dev)].buffer) - offset);
	result = uiomove(messagebuf[minor(dev)].buffer + offset, toread, myuio);
	if (result)
	  return result;
      }
    return 0;
  }

SYSINIT(foodev, SI_SUB_DRIVERS, SI_ORDER_MIDDLE+CDEV_MAJOR, &fooinit, NULL);

#endif /* NFOO */



If you're looking for more information, Julian Elischer <julian@whistle.com>
has commited several of his device driver models in to the FreeBSD
source tree. I suspect you'll find it appearing in the 3.0 development
tree shortly. Additionally, I will be posting them on my Web page, as soon
as I get around to reworking this document (and others) in to HTML.



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?199702241905.OAA00646>