Skip site navigation (1)Skip section navigation (2)
Date:      Sat, 22 Feb 1997 10:09:00 -0500
From:      "Brian J. McGovern" <mcgovern@spoon.beta.com>
To:        hackers@freebsd.org
Subject:   Device driver cookbook.
Message-ID:  <199702221509.KAA12718@spoon.beta.com>

next in thread | raw e-mail | index | archive | help
I promised I'd eventually get to it, and I'll be working on it heavily
this weeknd. Below is the first "installment" where I walk through a simple
pseudo-device driver that you can read from with a fixed buffer. Subsequent
additions will make it more flexable. I'm releasing it to "hackers" now, so
that everyone has a chance to comment, and let me know if they like/don't like/
would like me to change anything. Thanks for the comments ahead of time.

	-Brian
- - - - - - - - - - - The Document - - - - - - - - - - - - - - - - - 

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. This is because the
kernel does a certain amount of error checking ahead of time, and won't allow
reads or writes to be performed on not-open minor numbers (more on this later).

One note on minor numbers. MINOR numbers are used to keep track of seperate
entities within the same device driver. For instance, for serial devices
(the sio driver), the first com port will have a minor number 0, the next,
a minor number of 1, the next, a minor number of 2, and so on. This is how
one logical device will be kept descrete from the others, even though their
major numbers are the same. For our first device driver, we'll support only
one minor device (minor number 0).

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.

Next, we'll move on to solving this read issue, so you'll always get your
whole message, we'll also eventually talk about writing to your buffer (so
you can change your message), and finally, supporting multiple message buffers.




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