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: port-i386
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
===============================================================================