Subject: Re: How to write a driver for a new PCI device?
To: Nathan J. Williams <nathanw@MIT.EDU>
From: Brett Lymn <blymn@baea.com.au>
List: tech-kern
Date: 03/02/2000 16:44:11
According to Nathan J. Williams:
>
>In an ideal world we'd have a pci(9) manual page that described the
>interfaces avaliable to you, some driver templates to work from, and a
>handy manual of the tehcniques and pitfalls involved in writing a
>device driver. Not yet, though.
>
This is my stab at it, it is incomplete though; the skel.c file does
not exist for example, the document is not complete and it is fairly
i386 centric:
Comments/corrections/encouragement are all welcome ;-)
Writing a pseudo device.
1. Introduction
This document is meant to provide a guide to someone who wants to
start writing kernel drivers. The document covers the writing of a
simple pseudo-device driver. You will need to be familiar with
building kernels, makefiles and the other arcana involved in
installing a new kernel as these are not covered by this document.
Also not covered is kernel programming itself - this is quite
different to programming at the user level in many ways. Having
said all that, this document will give you the process that is
required to get your code into and recognised by the kernel.
Expect kernel panics once you get your code there :-)
2. Your code
The file pseudo_dev_skel.c gives the framework for a
pseudo-device. Note that, unlike a normal device driver, a
pseudo-device does not have a probe routine because this is not
necessary. This simplifies life because we do not need to deal
with the autoconfig framework. The skeleton file give is for a
pseudo-device that supports the open, close and ioctl calls. This
is about the minimum useful set of calls you can have in a real
pseudo-device. There are other calls to support read, write, mmap
and other device functions but they all follow the same pattern as
open, close and ioctl so they have been omitted for clarity.
Probably the first important decision you need to make is what you
are going to call your new device. This needs to be done up front
as there are a lot of convenience macros that generate kernel
structures by prepending your device name to the function call
names, will help if you have an idea of the config file entry you
want to have. The config file entry does not have to match the
source code file name. In our skeleton driver we have decided to
call the pseudo-device "skeleton", so we shall have a config file
entry called skeleton. This means that the attach, open, close and
ioctl function calls are named skeletonattach, skeletonopen,
skeletonclose and skeletoniotcl respectively. Another important
decision is what sort of device you are writing - either a
character or block device as this will affect how your code
interacts with the kernel and, of course, your code itself.
2.1 The functions
The kernel interfaces to your device via a set of function calls
which will be called when a user level program accesses your
device. A device need not support all the calls, as we will see
later, but at a minimum a useful device needs to support an open
and close on it. Remember the function names need to be prepended
with your device name. The functions are:
2.1.1 attach
This function is called once when the kernel is initialising. It is
used to set up any variables that are referenced in later calls or
for allocating kernel memory needed for buffers. The attach
function is passed on parameter which is the number of devices this
driver is expected to handle.
2.1.2 open
As the name suggests, this function will be called when a user level
programme performs an open(2) call on the device. At it's simplest
the open function may just return success. More commonly, the open
call will validate the request and may allocate buffers or
initialise other driver state to support calls to the other driver
functions. The open call is passed the following parameters:
dev
This is the device minor number the open is being
performed on.
flags
??? flags passed to the open call by user ???
mode
??? mode for open ???
proc
This is a pointer to the proc structure of the process
that has requested the open. It allows for validation
of credentials of the process.
2.1.3 close
This closes an open device. Depending on the driver this may be as
simple as just returning success or it could involve free'ing
previously allocated memory and/or updating driver state variables
to indicate the device is no longer open. The parameters for the
close function call are the same as those describe for open.
2.1.4 read
Read data from your device. The parameters for the function are:
dev
The minor number of the device.
uio
This is a pointer to a uio struct. The read function
will fill in the uio struct with the data it wants to
return to the user.
flags
??? wuffor ??
2.1.5 write
Write data to your device. The parameters for the write function
are the same as those for a read function - the only difference
being that the uio structure contains data to be written to the
device.
2.1.6 ioctl
Perform an ioctl on your device. The parameters for the ioctl call
are:
dev
The minor number of the device.
cmd
The ioctl command to be performed. The commands are
defined in a header file which both the kernel code
and the user level code reference. See the sample
header for an example.
data
This is a pointer to the parameters passed in by the
user level code. What is in this parameter depends on
the implementation of the ioctl and also on the actual
ioctl command being issued.
flags
??? wuffor ???
proc
The proc structure that is associated with the user
level process making the ioctl request.
2.1.7 stop
??? wuffor this ??? Stop output on tty style device??
tty
tty associated with the device????
flags
???
2.1.8 poll
Checks the device for data that can be read from it. The parameters
are:
dev
The minor number of the device used.
events
The event(s) that the user level call is polling for.
proc
The proc structure that is associated with the user
level process making the ioctl request.
2.1.9 mmap
Supports the capability of mmap'ing a driver buffer into a user
level programme's memory space. The parameters are:
dev
The minor device number of the device used.
offset
The offset from the start of the buffer at which to
start the mmap.
prot
The type of mmap to perform, either read only, write
only or read write. The device driver need not
support all modes.
3. Making the kernel aware of the new device
Once you have done the coding of your pseudo-device it is then time
to hook your code into the kernel so that it can be accessed. Note
that the process of hooking a pseudo-device into the kernel differs
a lot from that of a normal device. Since a pseudo-device is
either there or not the usual device probe and autoconfiguration is
bypassed and entries made into kernel structures at the source
level instead of at run time. To make the kernel use your code you
have to modify these files:
3.1 /usr/src/sys/sys/conf.h
This file contains some macro defines to set up the cdevsw
(character device switch) and bdevsw (block device switch) table
entries. You should, by now, know what type of device you are
writing. In our example the skeleton driver is a character device
so we want to create an entry. Also, our skeleton driver only
supports the open, close and ioctl calls. Looking through the
conf.h file we find there is a generic device defined called
cdev__oci_init which does exactly what we want. This saves us a
bit of typing by adding this define into conf.h:
#define cdev_skeleton_init(c,n) cdev__oci_init(c,n)
This defines a macro we can use to define the cdevsw entry in
another file. For a more complex driver you can just copy one of
the other defines and modify as required.
3.2 /usr/src/sys/arch/i386/i386/conf.c (where is on other archs?)
Once we have the cdevsw initialisation entry in conf.h we are set
to do the next step. The first thing to do is to include an
include file and set up the prototype for the devsw entry. We do
this by putting this code in:
#include "skeleton.h"
cdev_decl("skeleton")
Wait a minute! We haven't created a skeleton.h! That is correct,
we don't create that file. It will be created by config(8), we
shall see later how this is done. The second line there sets up
the function prototypes for the skeleton driver - you should
replace skeleton with the name of your pseudo-device. That takes
care of the declarations. Now we need to add the device into the
bdevsw/cdevsw table. Since skeleton is a cdev we need to find the
cdevsw array and add an entry to it. You should add an entry to
the end of the array - trying to add an entry in the middle of the
cdevsw table will mess up all the other device drivers. So, at the
end of the cdevsw table we add an entry like this:
cdev_skeleton_init(NSKELETON,
skeleton), /* 65: Skeleton pseudo-device */
Again, NSKELETON is not defined by us anywhere. When config(8) is
run it will generate a skeleton.h file with NSKELETON defined in
it, the symbol defines the number of these devices to create - the
number comes from the config file. Note that cdev_skeleton_init is
the macro we defined in conf.h and that the second parameter
("skeleton") is the name of our pseudo-driver. This macro
concatentates the name of the pseudo-driver with the function call
names (open, close, ioctl, etc) to produce the function names that
we have defined in our code. This is how the kernel knows to run
your code. The last bit of the puzzle is the number in the comment
next to the entry. You must copy the format of the other entries,
increment the number and put the function of your device into the
comment. The number is important. This number is the major number
of your device. You need to make a note of this number for later.
4. Making config(8) aware of the new device
5. Allowing user level programmes access to the new device
--
===============================================================================
Brett Lymn, Computer Systems Administrator, BAE SYSTEMS
===============================================================================