Writing a Control and Measurement Device Interface (COMEDI) driver

Published on: 2005-9-4

Home | Archive

Writing a Control and Measurement Device Interface (COMEDI) driver

2005-09-04T07:30:00

The Phoenix mailing list (phoenix-project@freelists.org) is becoming active. Georges Khaznadar had this interesting suggestion that if we wrap the COMEDI framework over the Phoenix driver, it would be easy to use programs like QtScope which don't care what the underlying DAQ hardware is as long as it is COMEDI-compliant. I spent an hour today exploring this idea [Note that Ajith's Phoenix library also does much the same stuff; but in a less `generic' way. COMEDI will be useful only if there are sufficient number of applications which use it's interface specification; make the Phoenix driver COMEDI-compliant and presto, these data acquisition applications suddenly become Phoenix-friendly]

What does COMEDI do

All data acquisition boards act the same conceptually - there will atleast be a few analog input channels and facility to:
  1. Select the channel
  2. Trigger start-of-conversion
  3. Know when the conversion has ended (polling or interrupts)
What differs is the way in which all these low-level operations are implemented. COMEDI helps us lay out a thin layer of code above the nitty-gritty stuff so as to make the job of the application developer simple.

Installing COMEDI

I downloaded comedi-0.7.70 and comedilib-0.7.22 from the project home page; installation was simple - the usual

./configure;make;make install
dance. COMEDI requires a few device files, so you have to do a `make dev' also. comedi-0.7.70 implements the kernel space layer including drivers for plenty of boards. comedilib is the user space library - you need it to invoke the kernel space routines.

Configuring COMEDI

On my machine, the COMEDI kernel space drivers get installed into:

/lib/modules/2.6.7/comedi/
You will find a module called `comedi.ko'; this is the `master' module which implements lots of `generic' stuff. Then you have plenty of .ko files like usbdux.ko, ni_labpc.ko etc - these are the board specific drivers. You should start off by reading the file `skel.c', a `dummy' driver which shows you how to code COMEDI-based drivers. The documentation which comes with comedilib (under doc/doc_html) is also very useful. To get started, lets try to make the dummy driver work. First, it will be nice if `modprobe' is able to load skel.ko and the file on which it depends, comedi.ko, automagically. Make sure that /lib/modules/2.6.7/modules.dep contains the line:

/lib/modules/2.6.7/comedi/skel.ko: /lib/modules/2.6.7/comedi/comedi.ko
Now, you can do a `modprobe skel' and both `skel.ko' and `comedi.ko' will be loaded. Just loading the required modules is not enough. You now have to associate a COMEDI device file with one specific driver, in this case, `skel': This is done using the comedi_config command:

comedi_config /dev/comedi0 skel-100
Run `dmesg' and make sure that you are getting some meaningful message from the kernel. Run the `info' command (under `demo' folder in `comedilib') and you will get plenty of information regarding the dummy driver.

Writing your own COMEDI driver

Here is a simple driver program (just skel.c minus lots of stuff).

#include <linux/comedidev.h>

// Not really required.

typedef struct {
	char *name;
	int ai_chans;
	int ai_bits;
	int have_dio;
}phoenix_board;

static phoenix_board phb = {"phoenix", 8, 8, 0};

static int phoenix_attach(comedi_device *dev, comedi_devconfig *it);
static int phoenix_detach(comedi_device *dev);
static int phoenix_ai_rinsn(comedi_device*, comedi_subdevice*,
	comedi_insn*, lsampl_t*);

static comedi_driver driver_phoenix = {
	driver_name: "phoenix",
	module: THIS_MODULE,
	attach: phoenix_attach,
	detach: phoenix_detach
};

static int phoenix_attach(comedi_device *dev, comedi_devconfig *it)
{
	comedi_subdevice *s;

	printk("Attach called: dev = %x\n", dev);
	dev->board_name = "Phoenix, NSC India";
	if(alloc_subdevices(dev, 1) < 0)
		return -ENOMEM;

	s = dev->subdevices+0;
	s->type = COMEDI_SUBD_AI;
	s->subdev_flags = SDF_READABLE | SDF_GROUND;
	s->n_chan = phb.ai_chans;
	s->maxdata = (1 << phb.ai_bits) - 1;
	s->range_table = &range_unipolar5;
	s->insn_read = phoenix_ai_rinsn;

	return 1;
}

static int phoenix_ai_rinsn(comedi_device *dev, comedi_subdevice *s,
comedi_insn *insn, lsampl_t *data)
{
	int i, n;
	printk("rinsn called: channel = %d\n", CR_CHAN(insn->chanspec));
	printk("samples to read = %d\n", insn->n);
	n = insn->n;
	for(i = 0; i < n; i++)
		data[i] = 1234;
	return insn->n;

}

static int phoenix_detach(comedi_device *dev)
{
	printk("detach called: dev = %x\n", dev);
	return 0;
}

COMEDI_INITCLEANUP(driver_phoenix);
Before we understand how it works, let's try to compile and install it. The driver source, `phoenix.c' should be placed under the comedi/drivers directory. Then, Makefile.am should be changed in two places: Once this is done, go to the root of the comedi source distribution and run `autoreconf'; this should generate a new `Makefile.in'. What remains is just a `make;make install'. Load both `comedi.ko' and `phoenix.ko' into memory using `modprobe' or `insmod' and execute:

comedi_config /dev/comedi0 phoenix
This will `attach' the phoenix driver to /dev/comedi0.

How does the code work?

Fairly simple. The structure `phoenix_driver' is the key; it contains address of two routines - one of which is called during driver attach (`attach' takes place when you run `comedi_config'). The macro COMEDI_INITCLEANUP does the job of doing whatever initialization is required to `register' this structure with the COMEDI generic layer. The generic layer builds up a structure of type `comedi_device' and passes it's address to `phoenix_attach' which does the job of filling up certain important fields with device specific info. The Phoenix box can be thought of as a data acquisition device with multiple subdevices, each subdevice having multiple channels. Thus, we have an analog input subdevice with 8 channels each of resolution 8 bits and 5V unipolar range. We also have an analog output subdevice with just one channel, 8 bits resolution and 5V bipolar range ... and so on. In the code above, I am setting up the number of subdevices to 1 (just the analog inputs). The line:

s->insn_read = phoenix_ai_rinsn;
is crucial. It set's up a function which will respond to various read operations from user space. Here is a user space test program:

#include <stdio.h>
#include <comedilib.h>

#define N 100

main()
{
	comedi_t *fd;
	//lsampl_t is a 32 bit object.
	lsampl_t data[N];
	int i, r = 33;
	int subdev = 0, channel = 0, range = 0;

	fd = comedi_open("/dev/comedi0");

	printf("%x\n", fd);
	//printf("%d\n", sizeof(lsampl_t));

	r = comedi_data_read_n(fd,subdev,channel,range,AREF_GROUND, data, N);
	printf("r = %d\n", r);
	for(i = 0; i < N; i++)
		printf("%d\n", data[i]);

}
To wind up, here is the output shown on my machine when I run the `info' program (present under the `demo' directory of comedilib source distro).

overall info:
  version code: 0x000746
  driver name: phoenix
  board name: Phoenix, NSC India
  number of subdevices: 1
subdevice 0:
  type: 1 (analog input)
  number of channels: 8
  max data value: 255
  ranges:
    all chans: [0,5]
  command:
    not supported

huseyinkozan

Thu Nov 20 20:08:24 2008

Thanks for your article.


Lincoln

Tue Oct 27 17:22:12 2009

Thank you for this dummy explaination