This Section explains the most important implementations aspects of the Comedi device drivers. It tries to give the interested device driver writer an overview of the different steps required to write a new device driver.
This Section does not explain all implementation details of the Comedi software itself: Comedi has once and for all solved lots of boring but indispensable infrastructural things, such as: timers, management of which drivers are active, memory management for drivers and buffers, wrapping of RTOS-specific interfaces, interrupt handler management, general error handling, the /proc interface, etc. So, the device driver writers can concentrate on the interesting stuff: implementing their specific interface card's DAQ functionalities.
In order to make a decent Comedi device driver, you must know the answers to the following questions:
How does the communication between user space and kernel space work?
What functionality is provided by the generic kernel-space Comedi functions, and what must be provided for each specific new driver?
How to use DMA and interrupts?
What are the addresses and meanings of all the card's registers?
This information is to be found in the so-called "register level manual" of the card. Without it, coding a device driver is close to hopeless. It is also something that Comedi (and hence also this handbook) cannot give any support or information for: board manufacturers all use their own design and nomenclature.
In user space, you interact with the functions implemented in the /usr/src/comedilib directory. Most of the device driver core of the Comedilib library is found in lib subdirectory.
All user-space Comedi
instructions and
commands
are transmitted to kernel space through a traditional
ioctl
system call.
(See /usr/src/comedilib/lib/ioctl.c.)
The user space information command is encoded as
a number in the ioctl
call, and decoded in the
kernel space library. There, they are executed by their kernel-space
counterparts. This is done in the
/usr/src/comedi/comedi/comedi_fops.c file: the
comedi_ioctl()
function processes the results of
the ioctl
system call, interprets its contents,
and then calls the corresponding kernel space
do_..._ioctl
function(s).
For example, a Comedi
instruction is further processed
by the do_insn_ioctl()
function. (Which, in turn,
uses parse_insn()
for further detailed processing.)
The data corresponding to instructions and commands is transmitted
with the copy_from_user()
system call;
acquisition data captured by the interface card passes the kernel-user
space boundary with the help of a copy_to_user()
system call.
The major include files of the kernel-space part of Comedi are:
include/linux/comedidev.h: the header file for kernel-only structures (device, subdevice, async (i.e., buffer/event/interrupt/callback functionality for asynchronous DAQ in a Comedi command), driver, lrange), variables, inline functions and constants.
include/linux/comedi_rt.h: all the real-time stuff, such as management of ISR in RTAI and RTLinux/Free, and spinlocks for atomic sections.
include/linux/comedilib.h: the header file for the kernel library of Comedi.
From all the relevant Comedi device driver code that is found in the /usr/src/comedi/comedi directory (if the Comedi source has been installed in its normal /usr/src/comedi location), the generic functionality is contained in two parts:
A couple of C files contain the infrastructural support. From these C files, it's especially the comedi_fops.c file that implements what makes Comedi into what people want to use it for: a library that has solved 90% of the DAQ device driver efforts, once and for all.
For real-time applications, the subdirectory kcomedilib implements an interface in the kernel that is similar to the Comedi interface accessible through the user-space Comedi library.
There are some differences in what is possible and/or needed in kernel space and in user space, so the functionalities offered in kcomedilib are not an exact copy of the user-space library. For example, locking, interrupt handling, real-time execution, callback handling, etc., are only available in kernel space.
Most drivers don't make use (yet) of these real-time functionalities.
This Section explains the generic data structures that a device driver interacts with:
typedef struct comedi_lrange_struct comedi_lrange; typedef struct comedi_subdevice_struct comedi_subdevice; typedef struct comedi_device_struct comedi_device: typedef struct comedi_async_struct comedi_async typedef struct comedi_driver_struct comedi_driver;They can be found in /usr/src/comedi/include/linux/comedidev.h. Most of the fields are filled in by the Comedi infrastructure, but there are still quite a handful that your driver must provide or use. As for the user-level Comedi, each of the hierarchical layers has its own data structures: channel (
comedi_lrange
),
subdevice, and device.Note that these kernel-space data structures have similar names as their user-space equivalents, but they have a different (kernel-side) view on the DAQ problem and a different meaning: they encode the interaction with the hardware, not with the user.
However, the comedi_insn and comedi_cmd data structures are shared between user space and kernel space: this should come as no surprise, since these data structures contain all information that the user-space program must transfer to the kernel-space driver for each acquisition.
In addition to these data entities that are also known at the user level (device, sub-device, channel), the device driver level provides two more data structures which the application programmer doesn't get in touch with: the data structure comedi_driver that stores the device driver information that is relevant at the operating system level, and the data structure comedi_async that stores the information about all asynchronous activities (interrupts, callbacks and events).
comedi_lrange
The channel information is simple, since it contains only the signal range information:
struct comedi_lrange_struct{ int length; comedi_krange range[GCC_ZERO_LENGTH_ARRAY]; };
comedi_subdevice
The subdevice is the smallest Comedi entity that can be used for "stand-alone" DAQ, so it is no surprise that it is quite big:
struct comedi_subdevice_struct{ int type; int n_chan; int subdev_flags; int len_chanlist; /* maximum length of channel/gain list */ void *private; comedi_async *async; void *lock; void *busy; unsigned int runflags; int io_bits; lsampl_t maxdata; /* if maxdata==0, use list */ lsampl_t *maxdata_list; /* list is channel specific */ unsigned int flags; unsigned int *flaglist; comedi_lrange *range_table; comedi_lrange **range_table_list; unsigned int *chanlist; /* driver-owned chanlist (not used) */ int (*insn_read)(comedi_device *,comedi_subdevice *,comedi_insn *,lsampl_t *); int (*insn_write)(comedi_device *,comedi_subdevice *,comedi_insn *,lsampl_t *); int (*insn_bits)(comedi_device *,comedi_subdevice *,comedi_insn *,lsampl_t *); int (*insn_config)(comedi_device *,comedi_subdevice *,comedi_insn *,lsampl_t *); int (*do_cmd)(comedi_device *,comedi_subdevice *); int (*do_cmdtest)(comedi_device *,comedi_subdevice *,comedi_cmd *); int (*poll)(comedi_device *,comedi_subdevice *); int (*cancel)(comedi_device *,comedi_subdevice *); int (*buf_change)(comedi_device *,comedi_subdevice *s,unsigned long new_size); void (*munge)(comedi_device *, comedi_subdevice *s, void *data, unsigned int num_bytes, unsigned int start_chan_index ); unsigned int state; };The function pointers
(*insn_read)
...
(*cancel)
.
offer (pointers to) the standardized
user-visible API
that every subdevice should offer; every device driver has to fill
in these functions with their board-specific implementations.
(Functionality for which Comedi provides generic functions will, by
definition, not show up in the device driver data structures.)The buf_change()
and munge()
functions offer functionality that is not visible to the user and for
which the device driver writer must provide a board-specific
implementation:
buf_change()
is called when a change in the
data buffer requires handling; munge()
transforms
different bit-representations of DAQ values, for example from
unsigned to 2's complement.
comedi_device
The last data structure stores the information at the device level:
struct comedi_device_struct{ int use_count; comedi_driver *driver; void *private; kdev_t minor; char *board_name; const void *board_ptr; int attached; int rt; spinlock_t spinlock; int in_request_module; int n_subdevices; comedi_subdevice *subdevices; int options[COMEDI_NDEVCONFOPTS]; /* dumb */ int iobase; int irq; comedi_subdevice *read_subdev; wait_queue_head_t read_wait; comedi_subdevice *write_subdev; wait_queue_head_t write_wait; struct fasync_struct *async_queue; void (*open)(comedi_device *dev); void (*close)(comedi_device *dev); };
comedi_async
The following data structure contains all relevant information: addresses and sizes of buffers, pointers to the actual data, and the information needed for event handling:
struct comedi_async_struct{ void *prealloc_buf; /* pre-allocated buffer */ unsigned int prealloc_bufsz; /* buffer size, in bytes */ unsigned long *buf_page_list; /* physical address of each page */ unsigned int max_bufsize; /* maximum buffer size, bytes */ unsigned int mmap_count; /* current number of mmaps of prealloc_buf */ volatile unsigned int buf_write_count; /* byte count for writer (write completed) */ volatile unsigned int buf_write_alloc_count; /* byte count for writer (allocated for writing) */ volatile unsigned int buf_read_count; /* byte count for reader (read completed)*/ unsigned int buf_write_ptr; /* buffer marker for writer */ unsigned int buf_read_ptr; /* buffer marker for reader */ unsigned int cur_chan; /* useless channel marker for interrupt */ /* number of bytes that have been received for current scan */ unsigned int scan_progress; /* keeps track of where we are in chanlist as for munging */ unsigned int munge_chan; unsigned int events; /* events that have occurred */ comedi_cmd cmd; // callback stuff unsigned int cb_mask; int (*cb_func)(unsigned int flags,void *); void *cb_arg; int (*inttrig)(comedi_device *dev,comedi_subdevice *s,unsigned int x); };
comedi_driver
struct comedi_driver_struct{ struct comedi_driver_struct *next; char *driver_name; struct module *module; int (*attach)(comedi_device *,comedi_devconfig *); int (*detach)(comedi_device *); /* number of elements in board_name and board_id arrays */ unsigned int num_names; void *board_name; /* offset in bytes from one board name pointer to the next */ int offset; };
The directory comedi contains a large set of support functions. Some of the most important ones are given below.
From comedi/comedi_fops.c, functions to handle the hardware events (which also runs the registered callback function), to get data in and out of the software data buffer, and to parse the incoming functional requests:
void comedi_event(comedi_device *dev,comedi_subdevice *s,unsigned int mask); int comedi_buf_put(comedi_async *async, sampl_t x); int comedi_buf_get(comedi_async *async, sampl_t *x); static int parse_insn(comedi_device *dev,comedi_insn *insn,lsampl_t *data,void *file);The file comedi/kcomedilib/kcomedilib_main.c provides functions to register a callback, to poll an ongoing data acquisition, and to print an error message:
int comedi_register_callback(comedi_t *d,unsigned int subdevice, unsigned int mask,int (*cb)(unsigned int,void *),void *arg); int comedi_poll(comedi_t *d, unsigned int subdevice); void comedi_perror(const char *message);The file comedi/rt.c provides interrupt handling for real-time tasks (one interrupt per device!):
int comedi_request_irq(unsigned irq,void (*handler)(int, void *,struct pt_regs *), unsigned long flags,const char *device,comedi_device *dev_id); void comedi_free_irq(unsigned int irq,comedi_device *dev_id)
The /usr/src/comedi/comedi/drivers subdirectory contains the board-specific device driver code. Each new card must get an entry in this directory. Or extend the functionality of an already existing driver file if the new card is quite similar to that implemented in an already existing driver. For example, many of the National Instruments DAQ cards use the same driver files.
To help device driver writers, Comedi provides the "skeleton" of a new device driver, in the comedi/drivers/skel.c file. Before starting to write a new driver, make sure you understand this file, and compare it to what you find in the other already available board-specific files in the same directory.
The first thing you notice in skel.c is the documentation section: the Comedi documentation is partially generated automatically, from the information that is given in this section. So, please comply with the structure and the keywords provided as Comedi standards.
The second part of the device driver contains board-specific static data structure and defines: addresses of hardware registers; defines and function prototypes for functionality that is only used inside of the device driver for this board; the encoding of the types and number of available channels; PCI information; etc.
Each driver has to register two functions which are called when you load and unload your board's device driver (typically via a kernel module):
mydriver_attach(); mydriver_detach();In the "attach" function, memory is allocated for the necessary data structures, all properties of a device and its subdevices are defined, and filled in in the generic Comedi data structures. As part of this, pointers to the low level instructions being supported by the subdevice have to be set, which define the basic functionality. In somewhat more detail, the
mydriver_attach()
function must:
check and request the I/O port region, IRQ, DMA, and other hardware resources. It is convenient here if you verify the existence of the hardware and the correctness of the other information given. Sometimes, unfortunately, this cannot be done.
allocate memory for the private data structures.
initialize the board registers and possible subdevices (timer, DMA, PCI, hardware FIFO, etc.).
return 1, indicating success. If there were any errors along the way,
you should return the appropriate error number. If an error is
returned, the mydriver_detach()
function is
called. The mydriver_detach()
function should
check any resources that may have been allocated and release them as
necessary. The Comedi core frees
dev->subdevices
and
dev->private
, so this does not need to be done in
detach
.
If the driver has the possibility to offer asynchronous data acquisition, you have to code an interrupt service routine, event handling routines, and/or callback routines.
mydriver_attach()
function needs most of your
attention, because it must correctly define and allocate the (private
and generic) data structures that are needed for this device. That is,
each sub-device and each channel must get appropriate data fields, and
an appropriate initialization. The good news, of course, is that
Comedi provides the data structures and the defines that fit very
well with almost all DAQ functionalities found on interface cards.
These can be found in the
header files of the
/usr/src/comedi/include/linux/
directory.Drivers for digital IOs should implement the following functions:
insn_bits()
: drivers set this if they have a
function that supports reading and writing multiple bits in a digital
I/O subdevice at the same time. Most (if not all) of the drivers use
this interface instead of insn_read and insn_write for DIO subdevices.
insn_config()
: implements INSN_CONFIG
instructions. Currently used for configuring the direction of digital
I/O lines, although will eventually be used for generic configuration
of drivers that is outside the scope of the currently defined Comedi
interface.
read
and write
functions for
the analog channels on the card:
insn_read()
: acquire the inputs on the board and
transfer them to the software buffer of the driver.
insn_write()
: transfer data from the software
buffer to the card, and execute the appropriate output conversions.
Implementation of all of the above-mentioned functions requires perfect knowledge about the hardware registers and addresses of the interface card. In general, you can find some inspiration in the already available device drivers, but don't trust that blind cut-and-paste will bring you far...
Continuous acquisition is tyically an asynchronous activity: the function call that has set the acquisition in motion has returned before the acquisition has finished (or even started). So, not only the acquired data must be sent back to the user's buffer "in the background", but various types of asynchronous event handling can be needed during the acquisition:
The hardware can generate some error or warning events.
Normal functional interrupts are generated by the hardware, e.g., signalling the filling-up of the card's hardware buffer, or the end of an acquisition scan, etc.
The device driver writer can register a driver-supplied "callback" function, that is called at the end of each hardware interrupt routine.
Another driver-supplied callback function is executed when the user program launches an INSN_INTTRIG instruction. This event handling is executed synchronously with the execution of the triggering instruction.
The interrupt handlers are registered through the functions mentioned before The event handling is done in the existing Comedi drivers in statements such as this one:
s->async->events |= COMEDI_CB_EOA | COMEDI_CB_ERRORIt fills in the bits corresponding to particular events in the comedi_async data structure. The possible event bits are:
A few things to strive for when writing a new driver:
Some DAQ cards consist of different "layers" of hardware, which can each be given their own device driver. Examples are: some of the National Instruments cards, that all share the same Mite PCI driver chip; the ubiquitous parallel port, that can be used for simple digital IO acquisitions. If your new card has such a multi-layer design too, please take the effort to provide drivers for each layer separately.
Your hardware driver should be functional appropriate to the resources allocated. I.e., if the driver is fully functional when configured with an IRQ and DMA, it should still function moderately well with just an IRQ, or still do minor tasks without IRQ or DMA. Does your driver really require an IRQ to do digital I/O? Maybe someone will want to use your driver just to do digital I/O and has no interrupts available.
Drivers are to have absolutely no
global variables, mainly because the existence of global variables
immediately negates any possibility of using the driver for two
devices. The pointer dev->private
should be used
to point to a structure containing any additional variables needed by
a driver/device combination.
Drivers should report errors and warnings via the
comedi_error()
function.
(This is not the same function as the user-space
comedi_perror() function.)
For integrating new drivers in the Comedi's source tree the following things have to be done:
Choose a sensible name for the source code file. Let's assume here that you call it "mydriver.c"
Put your new driver into "comedi/drivers/mydriver.c".
Edit "comedi/drivers/Makefile.am" and add "mydriver.ko" to the "module_PROGRAMS" list. Also add a line
mydriver_ko_SOURCES = mydriver.cin the alphabetically appropriate place.
Run ./autogen.sh in the top-level comedi directory. You will need to have (a recent version of) autoconf and automake installed to successfully run autogen.sh. Afterwards, your driver will be built along with the rest of the drivers when you 'make'.
If you want to have your driver included in the Comedi distribution (you definitely want to :-) ) send it to David Schleef
or Frank Hess for review and integration. Note your work must be licensed under terms compatible with the GNU GPL to be distributed as a part of Comedi.