Recent Changes - Search:

Tracking

Hardware

Software

Mega Plan

Etc.

I2C

We wrote a kernel driver to allow communications between the Gumstix and the Brainstem over the I2C bus. The driver writes Acroname packets to /dev/brainstem and, when appropriate, reads replies. We illustrate this process with a simple example, followed by a more detailed discussion of the final kernel driver.

Hello World of Device Drivers

In this part, we will create a very simple character device driver which allows a userland process to open an associated device file, and read from the device. The device returns "Hello World!". Sample code: Attach:hello-dev.tar.gz

A kernel module requires an initialization function for loading, and an exit function for unloading. These functions are passed to macro definitions in init.h.

 module_init(robot_dev_init);
 module_exit(robot_dev_exit);

Module Initialization

For a /dev/brainstem device which emulates the Brainstem router, we need a character device. Devices have a Major and Minor number associated with them, and are either block or character devices. For this example, a Major device number of 42 is used. In general, we define a major number of 42 for robot devices, and we use minor device number 1 for Acroname Brainstem devices. Other kernel drivers will receive a different minor number. The kernel module initialization function looks like:

 static int __init robot_dev_init(void)
 {
         int result;

         result = register_chrdev(42, "robot", &robot_fops);
         if (result)
         {
                printk(KERN_ERR "%s: Driver Initialization failed\n", __FILE__);
                return result;
         }

         return 0;
 }

Data Structures

The device driver requires several data structures. The file_operations structure contains a bunch of pointers to functions. For this example driver, our robot device just needs to support file open.

 static struct file_operations robot_fops = {
     .owner		= THIS_MODULE,
     .open		= robot_open,
 };

The brainstem_fops structure is a file_operations structure which contains pointers to functions which should handle file requests for Brainstem devices. All functions defined here are registered with the kernel. For this example, we are only interested in the the read function.

 static struct file_operations brainstem_fops = {
     .owner		= THIS_MODULE,
     .read		       = brainstem_read,
 }; 

Opening Our Device File

When our driver is loaded and a device with a major number of 42 is opened, the robot_open() function is called. The inode structures contains the minor number of the device file that the user is trying to open, and we perform the appropriate action based on this value. If the Brainstem file_operations structure's open variable points to a function, then call it. In the example driver, the file_operations open variable is not used. If the minor device does not have an open function, then return success, otherwise open the function. The code looks like:

 static int robot_open(struct inode *inode, struct file *filp)
 {
       switch (iminor(inode))
       {
           case 1:
              filp->f_op = &brainstem_fops;
              break;
           default:
              return -ENXIO;
        }

        if(filp->f_op && filp->f_op->open)
             return filp->f_op->open(inode, filp);

        return 0;
 }

Reading From Our Device File

After opening the device file, a userland process can read from it. Since the kernel operates in different memory space, we need to pass the size of the read buffer and a buffer to store the bytes. Also, we need to use the copy_to_user function to copy from kernel space to user space. After copying bytes to user space, return the number of bytes written. The code looks like:

 static ssize_t brainstem_read (struct file *file, char __user *buf, 
                                size_t count, loff_t *offset)
 { 
      char message[] = "Hello World!\n";
      if(count >  sizeof(message))
            if(copy_to_user(buf,message,sizeof(message)))
                  return 0; /* error */
           return (sizeof(message));
      else
           if(copy_to_user(buf,message,count))
                  return 0; /* error */
           return count;
 }

Unloading the Module

The exit function unloads the module from the kernel, and just unregisters the device's major number.

 static void __exit robot_dev_exit(void)
 { 
     unregister_chrdev(42,"robot");
 }

Building the Module

After downloading and installing the Gumstix buildroot on your local machine, create a directory gumstix-buildroot/build_arm/linux-2.6.11gum/drivers/i2c/robot. Inside this directory create a file called Kconfig that looks like:

 menu "Robot Hardware support"
          depends on I2C

 config I2C_ROBOT
        tristate
        default n

 config  ROBOT_BRAINSTEM
         tristate "Acroname Brainstem Driver"
         depends on I2C && EXPERIMENTAL
         select I2C_ROBOT
         help
            This is the driver for the Acroname Brainstem over I2C

 endmenu

Also, create gumstix-buildroot/build_arm/linux-2.6.11gum/drivers/i2c/robot/Makefile that looks like:

  obj-$(CONFIG_ROBOT_BRAINSTEM)   += robot.o

  ifeq ($(CONFIG_I2C_DEBUG_CHIP),y)
  EXTRA_CFLAGS += -DDEBUG
  endif

In gumstix-buildroot/build_arm/linux-2.6.11gum/drivers/i2c/Makefile, change

 obj-y                           += busses/ chips/ algos/

to

 obj-y                           += busses/ chips/ algos/ robot/ 

In gumstix-buildroot/build_arm/linux-2.6.11gum/drivers/i2c/Kconfig, change

 source drivers/i2c/algos/Kconfig
 source drivers/i2c/busses/Kconfig
 source drivers/i2c/chips/Kconfig

to

 source drivers/i2c/algos/Kconfig
 source drivers/i2c/busses/Kconfig
 source drivers/i2c/chips/Kconfig
 source drivers/i2c/robot/Kconfig

Now reconfigure the kernel.

 make menuconfig

Under "Device Drivers | I2C Support | Robot Hardware Support", select the "Acroname Brainstem Driver" to be built as a module. Exit, saving this change. Build the new driver as a module:

 make modules

Finally transfer the new filesystem to the Gumstix.

So Does it Work?

Over serial, login into the Gumstix as root. To run the driver, first load the module using insmod drivers/i2c/robot.ko. Then check that the module was loaded by running lsmod. Somewhere in the list, you should see the module robot listed. Now, create the /dev/brainstem device file (a one-time operation):

 mknod /dev/brainstem c 42 1 

First, load the robot module: insmod drivers/i2c/robot/robot.ko. Test the driver by reading from the new device file. You should see something like the following:

 # cat /dev/brainstem 
 Hello World! 
 Hello World! 
 Hello World!     

Press Ctrl-C to stop reading. Finally, remove the module from the kernel:

 rmmod robot 

Final I2C Device Driver

Our kernel driver was developed based on the following diagram:

The figure maps to files in the Linux kernel as follows:

  • Adapter specific code: /drivers/i2c/busses/i2c-pxa.c
  • Algorithm: /drivers/i2c/algos/i2c-algo-pxa.c
  • I2C-core: /drivers/i2c/i2c-core.c
  • I2C-dev: /drivers/i2c/i2c-dev.c

The I2C-dev block allows the userland program to access the Brainstem through the /dev interface. In particular, we use /dev/brainstem. With this setup, we can acccess the Brainstem through standard reads and writes. For example, to set servo 2 to position 200, this would suffice:

 char packet[] = {
       2, /* Brainstem Address */
       3, /* Data size */
       33, /* cmdSRV_ABS */
       2, /* Servo 2 */
       200 /* Servo Position 200 */
 };
 file = open("/dev/brainstem", O_RDWR); 
 write(file, packet, sizeof(packet));
 close(file);

The Brainstem libraries follow a similar constrcution.

The driver is accessed by creating an instance of a driver structure. This driver structure is then registered with the kernel through i2c-core functions. The driver structure contains a pointer to an i2c_adapter structure, which in turn contains a pointer to an i2c_algorithm data structure.

Our driver structure looks like:

struct i2c_driver {

  	struct module *owner;
 	char name[32];
 	int id;
 	unsigned int class;
 	unsigned int flags;
 	int (*attach_adapter)(struct i2c_adapter *);
 	int (*detach_adapter)(struct i2c_adapter *);
 	int (*detach_client)(struct i2c_client *);
 	int (*command)(struct i2c_client *client,unsigned int cmd, void *arg);
 	struct device_driver driver;
 	struct list_head list;

};

Upon instantiation, we set the following values:

  • Set .owner = THIS_MODULE
  • .name = "Brainstem Driver"
  • The ID is a unique value between 0xf000 and 0xffff.
  • Set .flags = I2C_DF_NOTIFY
  • The attach_adapter() callback function is called any time a new I2C adapter is added. When i2c_add_driver is called, this function is called for each adapter which is already registered with i2c-core. We need to keep an array of all I2C adapters.
  • detach_adapter() is called when an adapter is removed (i.e. module is unloaded)
  • detach_client() should just return 0 for our purposes.
  • command() should return -1.

The next step is to define the i2c_adapter structure:

 struct i2c_adapter {
 	struct module *owner;
 	unsigned int id;	unsigned int class;
 	struct i2c_algorithm *algo;
 	void *algo_data;
 	int (*client_register)(struct i2c_client *);
 	int (*client_unregister)(struct i2c_client *);
 	struct semaphore bus_lock;
 	struct semaphore clist_lock;
 	int timeout;
 	int retries;
 	struct device dev;		/* the adapter device */
 	struct class_device class_dev;	/* the class device */
 	int nr;
 	struct list_head clients;
 	struct list_head list;
 	char name[I2C_NAME_SIZE];
 	struct completion dev_released;
 	struct completion class_dev_released;
 };

We'll have an array of these things, with 1 of them for each I2C bus. The array is filled from the attach_adapter() callback function from above. We only have 1 bus, so our array will be small. But our driver needs to be written so that it will work when more than one I2C bus is present.

An i2c_adapter has a pointer to an i2c_algorithm structure:

 struct i2c_algorithm {
 	char name[32];				
 	unsigned int id;
 	int (*master_xfer)(struct i2c_adapter *adap,struct i2c_msg msgs[], int num);
 	int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr, unsigned short flags, 
  	     char read_write, u8 command, int size, union i2c_smbus_data * data);
 	int (*slave_send)(struct i2c_adapter *,char*,int);
 	int (*slave_recv)(struct i2c_adapter *,char*,int);
 	int (*algo_control)(struct i2c_adapter *, unsigned int, unsigned long);
 	u32 (*functionality) (struct i2c_adapter *);
 };

Since none of the existing drivers have slave_recv() implemented, we developed our own.

Although you have direct access to an algorithm's master_xfer() function, the wrapper functions in i2c-core should be used instead. There is some locking which must occur, to avoid problems. The wrapped functions take care of this.

  • int i2c_master_recv(struct i2c_client *client, char *buf ,int count)
  • int i2c_master_send(struct i2c_client *client,const char *buf ,int count)
  • int i2c_transfer(struct i2c_adapter * adap, struct i2c_msg msgs[],int num)
  • int i2c_control(struct i2c_client *client, unsigned int cmd, unsigned long arg)
  • etc.

Interrupt handler

The driver required an interupt handler to manage reads and writes. We needed both a top half for master reads, and a bottom half for slave receives.

Master Reads In i2c-algo-pxa, master_xfer() calls i2c_pxa_do_xfer() which calls adap->wait_for_interrupt(). wait_for_interrupt points to i2c_pxa_handler(). So the way a master read works is:

  1. Wait until bus is free
  2. Write the slave address to the IDBR (I2C Data Buffer Register)
  3. Send a start condition
  4. Transfer a byte (the slave address) by calling adap->transfer()
  5. Call wait_for_interrupt()
    • At this point, the kernel switches to other tasks. It waits a really long time (relatively speaking) until an interupt happens
  6. Call i2c_pxa_readbytes() to read the bytes into a buffer
    • Loop
      • Call adap->transfer() to initiate the transfer of a byte
      • Call adap->wait_for_interrupt() to wait for it to actually show up
      • Call adap->read_byte() read the value of the byte, and then stuff it in a buffer
  7. Send a stop condition

The wait_for_interrupt() puts the current task on a wait queue. It will stay on the wait queue until an I2C interrupt occurs.

Slave Receive The approach taken for master reads won't work for slave receives. We can't call wait_for_interrupt() from within an interrupt handler (top half). What's needed is a bottom half for the interrupt handler. The bottom half is called after returning from the interrupt. Using drivers/block/floppy.c as a model, the interrupt handler does:

  • DECLARE_WORK(i2c_pxa_work, NULL, NULL)
    • i2c_pxa_work is created by this macro
  • PREPARE_WORK(&i2c_pxa_work, (void *)i2c_pxa_slave_receive);
    • Point the work structure to the function which needs to be run
  • Schedule_work(&i2c_pxa_work);
    • This puts the work on the work queue

So, we clear the Slave Address Received flag in the top half of the interrupt handler, and then schedule slave_receive work which should be run as the bottom half. Then return from the interrupt.

At this point, the bottom half can go through and do things pretty much the way a master receive does.

Edit - History - Print - Recent Changes - Search
Page last modified on June 18, 2005, at 02:28 PM