From owner-freebsd-hackers Mon Feb 24 11:06:03 1997 Return-Path: Received: (from root@localhost) by freefall.freebsd.org (8.8.5/8.8.5) id LAA08744 for hackers-outgoing; Mon, 24 Feb 1997 11:06:03 -0800 (PST) Received: from bmcgover-pc.cisco.com (bmcgover-pc.cisco.com [171.69.104.147]) by freefall.freebsd.org (8.8.5/8.8.5) with ESMTP id LAA08720 for ; Mon, 24 Feb 1997 11:05:43 -0800 (PST) Received: from bmcgover-pc.cisco.com (localhost.cisco.com [127.0.0.1]) by bmcgover-pc.cisco.com (8.8.5/8.8.2) with ESMTP id OAA00646 for ; Mon, 24 Feb 1997 14:05:08 -0500 (EST) Message-Id: <199702241905.OAA00646@bmcgover-pc.cisco.com> To: hackers@freebsd.org Subject: Pseudo-driver cookbook (phase II) Date: Mon, 24 Feb 1997 14:05:08 -0500 From: Brian McGovern Sender: owner-hackers@freebsd.org X-Loop: FreeBSD.org Precedence: bulk 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 #include #include #include #include #include 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 #include #include #include #include #include #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 #include #include #include #include #include #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 #include #include #include #include #include /* 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 #include #include #include #include #include /* 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 #include #include #include #include #include /* 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 #include #include #include #include #include /* 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 #include #include #include #include #include /* 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 #include #include #include #include #include #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) 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 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.