How to write a Platform Device/Driver - ADC Driver using wm97xx codec

Posted on December 13, 2013
Tags:
by Sanchayan Maity

Somewhat more than two years back, while I was working in Godrej, a senior colleague from my development team gave us a lecture on how to write character drivers for Linux. Didn’t understand one single thing, but since that time I have been trying to learn Linux kernel related stuff, with device drivers being the main area of focus. Of course, in between there have lot of lulls where I have hit a road block and then gone into a depression mode and then started again from where I left off, with renewed zeal and vigor.

One of the road blocks till date has been the inability to understand or picture the driver framework. Recently, after quiet a lot of effort the platform device/driver framework (may be framework isn’t the correct technical term, but, anyways) became clear to me.

Let me give you a background on what I was trying to write and achieve. My company Toradex manufactures and sells embedded computer on modules. One of them is the Colibri T20 which has the NVidia Tegra 2 processor on it. There is WM9715 codec from Wolfson Microelectronics, which is an audio codec with a touch panel controller. This codec has four auxilary ADC’s and the device is connected on an AC97 bus. These four ADC’s can be used as general purpose ADC’s, while also being used for audio and touchscreen functionality.

Now, my company provides WinCE and Linux for the modules, but, the Linux is hardly as well supported as WinCE. With the default Linux image which is provided, there is no support for being able to use all the four ADC’s. Only two ADC’s can be used which are made available through the power driver framework. Now what I mean by power driver framework is difficult to explain, and doing so would result in digressing from the main topic. Don’t worry though, this will not affect the purpose of this tutorial. For wm9715, there is already a driver available which provides the touch screen functionality along with providing access to auxiliary ADC’s. But, you need to expose the interface through some other driver or framework, for it to be usable as a general purpose ADC.

I am giving three links below and I will be using these through out the rest of the tutorial, by referring to their names instead of the URL’s.

  1. Header file: http://git.toradex.com/cgit/linux-toradex.git/tree/include/linux/wm97xx.h?h=tegra

  2. Core Driver file: http://git.toradex.com/cgit/linux-toradex.git/tree/drivers/input/touchscreen/wm97xx-core.c?h=tegra

  3. Board File: http://git.toradex.com/cgit/linux-toradex.git/tree/arch/arm/mach-tegra/board-colibri_t20.c?h=tegra

Of these, the first two files you can also find in the mainline Linux kernel.

Learn about the driver model from here, http://lxr.free-electrons.com/source/Documentation/driver-model/

N.B. It’s assumed that you have a decent knowledge of C, Linux, Character drivers, Cross compilation and Makefile.

The first question most people will ask is, what’s the use of a platform device/driver framework. For x86 based PC’s, it is possible for the OS to know what all devices are present, as ACPI or USB devices make it possible for the OS to query and find out which devices are present on the system, on which it is booting. For example, a device which is attached to a PCI or USB bus, the OS can query and find out what the device does or what are it’s capabilities, is it a graphics card or removable USB mass storage device.

For embedded systems, you have buses like I2c or SPI. Devices attached to these buses are not discoverable in the above sense as I tried to explain. The OS has to be explicitly told that, for example, a RTC is connected on the I2C bus at an address of 0x68. This is where the platform device/driver comes in the picture.

A board file is created for each board, which specifies the devices present on buses such as SPI and I2c. Have a look at the board file. You will find various platform devices and platform data structures which are used to register devices and relevant data with the OS. Platform data is used at later point when the OS is booting to know some specific details about the device. I will explain how the platform device and it’s driver are bound to each other in a while.

Have a look at the core driver file. The first two functions to look out for are the wm97xx_probe and wm97xx_remove. These are the two common operations (probe and remove) for any platform device and driver. You can see there is a device_driver structure at the bottom of the file. Whenever a module is loaded, the first function to be called is the _init() function. In this function, the driver is registered with a call to driver_register and passing it the device_driver structure. On a call to device_register() the probe function will be called. It will do all the necessary set up required. For this example, it is allocating memory, setting up handlers, allocating devices which will be using this core driver and registering it with the input subsystem, which is used for providing the touch screen functionality. Now, this basically remains the same, but might vary slightly, for example, a true ADC driver will register itself with the Industrial IO framework (IIO).

Now, here comes the main part. For me to use the ADC’s through a platform driver, I also need a platform device. So, I added a few function calls inside the probe() function for allocating platform device. This device I will be using later, how, we will come to that in a moment.

wm->colibriadc_dev = platform_device_alloc("colibri_adc", -1);
if (!wm->colibriadc_dev) {
ret = -ENOMEM;
    goto adc_err;
}
platform_set_drvdata(wm->colibriadc_dev, wm);
wm->colibriadc_dev->dev.parent = dev;
ret = platform_device_add(wm->colibriadc_dev);
if (ret < 0)
    goto adc_reg_err;

For error handling,

adc_reg_err:
    platform_device_put(wm->colibriadc_dev);
adc_err:
    platform_device_del(wm->colibriadc_dev);

Now, when the OS boots and registers and makes provision for this core driver, it will also allocate and make provision for my platform device which I created, to use this core driver. I added a platform_device pointer to the struct wm97xx which is in the header file.

struct platform_device *colibriadc_dev;

This pointer holds the return value of platform_device_alloc(). After the above changes, we are ready for the platform driver which we will use in conjunction with a character driver for accessing the ADC’s.

#include "linux/kernel.h"
#include "linux/module.h"
#include "linux/init.h"
#include "linux/platform_device.h"
#include "linux/gpio.h"
#include "linux/fs.h"
#include "linux/errno.h"
#include "asm/uaccess.h"
#include "linux/wm97xx.h"
#include "linux/kdev_t.h"
#include "linux/device.h"
#include "linux/cdev.h"
#include "linux/slab.h"
#include "linux/ioctl.h"

typedef struct
{
    unsigned int channelNumber;
    unsigned int adcValue;
}adcData;

#define SET_ADC_CHANNEL        _IOW('q', 1, adcData *)
#define GET_ADC_DATA           _IOR('q', 2, adcData *)

static dev_t first;         // Global variable for the first device number
static struct cdev c_dev;   // Global variable for the character device structure
static struct class *cl;    // Global variable for the device class
static struct wm97xx *wm;
static int init_result;
static int adcChannel;
static int adcValue;

static ssize_t adc_read(struct file* F, char *buf, size_t count, loff_t *f_pos)
{
    return -EPERM;
}

static ssize_t adc_write(struct file* F, const char *buf, size_t count, loff_t *f_pos)
{
    return -EPERM;
}

static int adc_open(struct inode *inode, struct file *file)
{
    return 0;
}

static int adc_close(struct inode *inode, struct file *file)
{
    return 0;
}

static long adc_device_ioctl(struct file *f, unsigned int adc_channel, unsigned long arg)
{
    adcData adc;

switch(adc_channel)
{
    case SET_ADC_CHANNEL:
    if (copy_from_user(&adc, (adcData*)arg, sizeof(adcData)))
    {
        return -EFAULT;
    }
    adcChannel = adc.channelNumber;
    break;

    case GET_ADC_DATA:
        switch (adcChannel)
        {
            case 1:
                adcValue = wm97xx_read_aux_adc(wm, WM97XX_AUX_ID1);
                break;

            case 2:
                adcValue = wm97xx_read_aux_adc(wm, WM97XX_AUX_ID2);
                break;

            case 3:
                adcValue = wm97xx_read_aux_adc(wm, WM97XX_AUX_ID3);
                break;

            case 4:
                adcValue = wm97xx_read_aux_adc(wm, WM97XX_AUX_ID4);
                break;

            default:
                return -EINVAL;
                break;
        }

        adc.channelNumber = adcChannel;
        adc.adcValue = adcValue;
        if (copy_to_user((adcData*)arg, &adc, sizeof(adcData)))
        {
            return -EFAULT;
        }
        printk(KERN_ALERT "AUX ADC%d reading: %d\n", adcChannel, adcValue);
        break;

        default:
        break;
    }

    return 0;
}

static int sample_wm97xx_probe(struct platform_device *pdev)
{
    wm = platform_get_drvdata(pdev);

    if (wm == NULL)
    {
        printk(KERN_ALERT "Platform get drvdata returned NULL\n");
        return -1;
    }

    return 0;
}

static int sample_wm97xx_remove(struct platform_device *pdev)
{
    /* http://opensource.wolfsonmicro.com/content/using-auxadc-wm97xx-touchscreen-drivers */

    return 0;
}

static struct platform_driver sample_wm97xx_driver = {
    .probe  = sample_wm97xx_probe,
    .remove = sample_wm97xx_remove,
    .driver = {
        .name = "colibri_adc",
        .owner = THIS_MODULE,
    },
};

static struct file_operations FileOps =
{
    .owner                = THIS_MODULE,
    .open                 = adc_open,
    .read                 = adc_read,
    .write                = adc_write,
    .release              = adc_close,
    .unlocked_ioctl        = adc_device_ioctl,
};

static int sample_wm97xx_init(void)
{
   init_result = platform_driver_probe(&sample_wm97xx_driver, &sample_wm97xx_probe);

   if (init_result < 0)
   {
       printk(KERN_ALERT "ADC Platform Driver probe failed with :%d\n", init_result);
       return -1;
   }
   else
   {
       init_result = alloc_chrdev_region( &first, 0, 1, "adc_drv" );
       if( 0 > init_result )
       {
           platform_driver_unregister(&sample_wm97xx_driver);
           printk( KERN_ALERT "ADC Device Registration failed\n" );
           return -1;
        }
       if ( (cl = class_create( THIS_MODULE, "chardev" ) ) == NULL )
       {
           platform_driver_unregister(&sample_wm97xx_driver);
           printk( KERN_ALERT "ADC Class creation failed\n" );
           unregister_chrdev_region( first, 1 );
           return -1;
    }

    if( device_create( cl, NULL, first, NULL, "adc_drv" ) == NULL )
    {
        platform_driver_unregister(&sample_wm97xx_driver);
        printk( KERN_ALERT "ADC Device creation failed\n" );
        class_destroy(cl);
        unregister_chrdev_region( first, 1 );
        return -1;
    }

    cdev_init( &c_dev, &FileOps );

    if( cdev_add( &c_dev, first, 1 ) == -1)
    {
        platform_driver_unregister(&sample_wm97xx_driver);
        printk( KERN_ALERT "ADC Device addition failed\n" );
        device_destroy( cl, first );
        class_destroy( cl );
        unregister_chrdev_region( first, 1 );
        return -1;
    }
    return 0;
}

static void sample_wm97xx_exit(void)
{
    platform_driver_unregister(&sample_wm97xx_driver);
    cdev_del( &c_dev );
    device_destroy( cl, first );
    class_destroy( cl );
    unregister_chrdev_region( first, 1 );

    printk(KERN_ALERT "ADC Driver unregistered\n");
}

module_init(sample_wm97xx_init);
module_exit(sample_wm97xx_exit);

MODULE_AUTHOR("Sanchayan Maity");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Colibri T20 ADC Driver");
CROSS_COMPILE ?= /home/sanchayan/Toradex/gcc-linaro/bin/arm-linux-gnueabihf-
ARCH          ?= arm
SOURCE_DIR    ?= /home/sanchayan/Toradex/T20V2.0/linux-toradex

AS          = $(CROSS_COMPILE)as
LD          = $(CROSS_COMPILE)ld
CC          = $(CROSS_COMPILE)gcc
CPP         = $(CC) -E
AR          = $(CROSS_COMPILE)ar
NM          = $(CROSS_COMPILE)nm
STRIP       = $(CROSS_COMPILE)strip
OBJCOPY     = $(CROSS_COMPILE)objcopy
OBJDUMP     = $(CROSS_COMPILE)objdump

obj-m     += adc_test.o
ccflags-y += -I$(SOURCE_DIR)/arch/arm

all:
    make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(SOURCE_DIR) M=$(PWD) modules

clean:
    rm *.o *.ko *.symvers *.order

The driver file and makefile source are shown above. On loading the module, the _init() will be called, which will in turn call platform_driver_register(). This will call the probe() function. If you really noticed carefully, then you can see the driver has the same name viz. “colibri_adc” which is also the name I passed for device allocation inside the probe() call of core driver. This is how the platform device will bind to a platform driver. They will have the same names and if you try to do a platform driver for which a platform device was not allocated, you will get a no device exists error.

One of the important things to note, is the call to platform_set_drvdata() in the probe call of core driver. It’s not like I have traced all functions, but, from what I understand this establishes a linking between the device allocated by the core driver and your driver, which will later allow you access to the device pointer, which you need to pass to the functions in the core driver, to use them.

You can see that the functions in core driver take a pointer to struct wm97xx. From where are you suppose to get this pointer? This you will get in the probe call of the driver which we wrote, by calling platform_get_drvdata(). You can see that this is so, in the driver code, where it is assigned to a static global pointer. The reason for using a static global pointer is I want this pointer to be accessible in the ioctl() calls, which will be the final interface for the user, which ultimately calls the core driver functions, by passing the required wm97xx pointer. On module unloading, _exit() will be called, which in turn calls platform_driver_unregister() which will result in a call to remove() function of the driver. Now, here there is not really anything to do in remove, as we do not require any kind of memory deallocation or clean up work for the platform part of the driver. You can have a nice look at the probe and remove functions of the core driver for getting an idea of what is really done in real world driver.

So, now we finally have access to the auxiliary ADC’s!!!. Hurray! And we also have our first (at least my first) platform device/driver. The user space application is below and is pretty simple.

#include "stdio.h"
#include "fcntl.h"
#include "linux/ioctl.h"

typedef struct
{
    unsigned int channelNumber;
    unsigned int adcValue;
}adcData;

#define SET_ADC_CHANNEL        _IOW('q', 1, adcData *)
#define GET_ADC_DATA         _IOR('q', 2, adcData *)

int main(void)
{
    int fd;
    int choice;
    int adc_channel;
    int retVal;
    int loop = 1;
    adcData adc;

    fd = open( "/dev/adc_drv", O_RDWR );

   if( fd < 0 )
   {
       printf("Cannot open device \t");
       printf(" fd = %d \n",fd);
       return 0;
   }

   while (loop)
   {
       printf("1: Read 2:Exit\n");
       printf("Enter choice:\t");
       scanf("%d", &choice);

       switch(choice)
       {
           case 1:
               printf("Enter ADC Channel Number:\t");
               scanf("%d", &adc_channel);
               adc.channelNumber = adc_channel;
               adc.adcValue = 0;
               retVal = ioctl(fd, SET_ADC_CHANNEL, &adc);
               if (retVal < 0)
               {
                   printf("Error: %d\n", retVal);
               }
               else
               {
                   retVal = ioctl(fd, GET_ADC_DATA, &adc);
                   if (retVal < 0)
                   {
                       printf("Error: %d\n", retVal);
                   }
                   else
                   {
                       printf("ADC Channel: %d Value: %d\n", adc.channelNumber, adc.adcValue);
                   }
               }
               break;

               case 2:
                   loop = 0;
                   break;

                default:
                    break;
        }
    }

    if( 0 != close(fd) )
    {
        printf("Could not close device\n");
    }

    return 0;
}

Do note that this is only an example, and what you might need to do will depend on what is available and what is not. Also, the code is not exactly up to the mark as per coding standards, but, I was too excited while working and rolling this out, so. I hope this clears the idea of how to use platform device/driver. Also, just in case it is not clear to people who are starting out, the core driver and header files are being changed. This will require a kernel recompilation and updating the uImage on the module, for our driver to work.

I will be putting another example of a PWM driver, which will be slightly bit more involved and should serve as another example.