From owner-freebsd-hackers Sat Feb 22 07:09:12 1997 Return-Path: Received: (from root@localhost) by freefall.freebsd.org (8.8.5/8.8.5) id HAA06950 for hackers-outgoing; Sat, 22 Feb 1997 07:09:12 -0800 (PST) Received: from spoon.beta.com (root@[199.165.180.33]) by freefall.freebsd.org (8.8.5/8.8.5) with ESMTP id HAA06895 for ; Sat, 22 Feb 1997 07:09:03 -0800 (PST) Received: from spoon.beta.com (mcgovern@localhost [127.0.0.1]) by spoon.beta.com (8.8.4/8.6.9) with ESMTP id KAA12718 for ; Sat, 22 Feb 1997 10:09:00 -0500 (EST) Message-Id: <199702221509.KAA12718@spoon.beta.com> To: hackers@freebsd.org Subject: Device driver cookbook. Date: Sat, 22 Feb 1997 10:09:00 -0500 From: "Brian J. McGovern" Sender: owner-hackers@freebsd.org X-Loop: FreeBSD.org Precedence: bulk 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 #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. 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 #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. 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.