How to write a Linux driver for the GPIO device on an OMAP-L138 board

“TI’s OMAP-L138 processor is a low-power applications processor based on an ARM926EJ-S and a C674x DSP core”. It provides significantly low power and has been widely used in the industry. In this article I will show you how to write a Linux driver for the GPIO device on a board based on this processor. We will use the AD5420 DAC (Digital to Analog) converter for this demonstration.

Before writing any code, let’s learn something about the hardware background. As we can see from the datasheet of AD5420, to make it work, all we need to do is to control the three special pins (suppose all pins have been connected correctly), labeled as LATCH, SCLK, and SDIN. As we all know, the GPIO peripheral provides general-purpose pins that can be configured as either inputs or outputs. The OMAP-L138 processor has up to 9 banks of GPIO pins, with each bank containing 16 pins. In this project, we used the 3nd bank of pins to connect with the DAC converter. They are GPIO2_10, GPIO2_11, and GPIO2_12 respectively.

First of all, we have to configure the PINMUX. The OMAP-L138 development kit has already provided an example. We just need to modify two files. In the file “/mach-davinci/include/mach/mux.h”, we will add DA850_GPIO2_10, DA850_GPIO2_11, and DA850_GPIO2_12 to the “enum davinci_da850_index” section. And in the file “arch/arm/mach-davinci/da850.c”, we add the following codes:

MUX_CFG(DA850, GPIO2_10, 5, 20, 15, 8, false) // connected with SCLK
MUX_CFG(DA850, GPIO2_11, 5, 16, 15, 8, false) // connected with LATCH
MUX_CFG(DA850, GPIO2_12, 5, 12, 15, 8, false) // connected with SDIN

The format of the MUX_CFG is (for a thorough understanding, we can check the processor’s technical reference manual):

DA850 -> SOC name
GPIO2_10 -> Pin desc
5 -> PINMUX5
20 -> Offset value of PINMUX5
15 -> Mask default value
8 -> Mode
false -> for debugging 

We also need to check that the GPIO driver is enabled in the Linux kernel. Then we rebuild the kernel.

Here we can write the actual device drive. A Linux drive consists of three main operations, there are “ioctl”, “read”, and “write”. And we also should implement the “init_module” and “cleanup_module” functions. And we’d better provide the statement of “MODULE_LICENSE”, ” MODULE_DESCRIPTION ” and “MODULE_AUTHOR” at the end of the source code. The following is the complete source code. Some comments have been added to make it readable.

[pre]



/* 
 *  Linux driver for AD5420
 * 
 *  12-Bit, Serial Input, 4 mA to 20 mA   
 * 
 *  The system contains 6 devices, so we will use the daisy-chain mode
 *
 */

#include 	<linux/module.h>
#include 	<linux/version.h>
#include 	<linux/delay.h>
#include 	<linux/irq.h>
#include 	<linux/interrupt.h>
#include 	<linux/completion.h>
#include    <mach/cputype.h>
#include    <mach/hardware.h>
#include    <mach/mux.h>
#include    <asm/gpio.h>
#include    <asm/uaccess.h>
#include 	<linux/fs.h>

#define  DEVICE_NAME      "/dev/myAD5420"   // the name of the device
#define  DEVICE_MAJOR     253    // major number of the device
#define  MAX_DEVICE_NUM   6      // 6 AD5420 chips in the daisy-chain mode
#define  SCLK_PIN         42     // connected to GPIO 2[10]
#define  LATCH_PIN        43     // connected to GPIO 2[11]
#define  SDIN_PIN         44     // connected to GPIO 2[12]
#define  CMD_BIT_NUM      8      // 8 bits
#define  DATA_BIT_NUM     16     // 16 bits    
#define  TOTAL_PIN_NUM    3      // 3 pins used

static int          GPIO_PIN_NUMS[TOTAL_PIN_NUM]  =   {42,43,44};
                                  // 2*16 + 10, 2*16 + 11, 2*16 + 12

struct  data_unit{
    unsigned char device_index;       // between [0,MAX_DEVICE_NUM-1]
    unsigned int  out_value;          // 12 bits valid
};

static  unsigned int  global_value_array[MAX_DEVICE_NUM];
static int ad5420_ioctl(struct inode *inode, struct file *file,
                        unsigned int command, unsigned long args);
static ssize_t ad5420_read(struct file *, char *, size_t, loff_t *);
static ssize_t ad5420_write(struct file *, char *, size_t, loff_t *);

static struct file_operations fops = {
    ioctl: ad5420_ioctl,
    read:  ad5420_read,
    write: ad5420_write
};

void   InitDevice(void);        
void   WriteUnit(unsigned int out_value);
void   ShiftCommand(unsigned char command);
void   ShiftData(unsigned int data);
void   OutPutData();

int init_module()
{
    int i,status,major;

    // configure the pins
    for(i = 0; i < TOTAL_PIN_NUM; ++i) {
        status = davinci_cfg_reg(GPIO_PIN_NUMS[i]);
        if (status < 0) {
            printk("Pin could not be muxed for GPIO functionality %d\n",
                       GPIO_PIN_NUMS[i]);  // in the driver we use printk
            return status;
        }
    }

    // request GPIO resource
    char gpio_pin_name[100];
    for(i = 0; i < TOTAL_PIN_NUM; ++i) {
        sprintf(gpio_pin_name, "gpio_%d", GPIO_PIN_NUMS[i]);
        status = gpio_request(GPIO_PIN_NUMS[i], gpio_pin_name);
        if (status < 0) {
            printk("Can not open GPIO %d\n", GPIO_PIN_NUMS[i]);
            return status;
        }
    }

    // set GPIO's input/output directions
    for(i = 0; i < TOTAL_PIN_NUM; ++i)
        gpio_direction_output(GPIO_PIN_NUMS[i] ,0);

    // set initial values
    for(i = 0; i < TOTAL_PIN_NUM; ++i)
        gpio_set_value(GPIO_PIN_NUMS[i] ,0);

    InitDevice();

    // register the device
    major = register_chrdev(DEVICE_MAJOR, DEVICE_NAME, &fops);
    if (major < 0) {
        printk ("Registering the character device failed with %d\n", major);
        return major;
    }

    // clean the global value array
    for(i = 0; i < TOTAL_PIN_NUM; ++i)
        global_value_array[i] = 0;

    return 0;
}

void cleanup_module(void)
{
    int i;

    // release GPIO resource
    for(i = 0; i < TOTAL_PIN_NUM; ++i)
        gpio_free(GPIO_PIN_NUMS[i]);

    // unregister the device
    unregister_chrdev(DEVICE_MAJOR, DEVICE_NAME); 
}

//  AD5420 can not read, so we let it empty
static ssize_t ad5420_read(struct file *filp,char *buf,
                           size_t count,loff_t *f_ops)
{       
         return 0; 
}

// output all values in the daisy-chain
static ssize_t ad5420_write(struct file *filp,char *buf,
                            size_t count,loff_t *f_ops)
{
    int i;
    unsigned int current_value;

    gpio_set_value(LATCH_PIN, 0);   
    gpio_set_value(SCLK_PIN, 0);   
    ndelay(50000);

    for(i = 0; i < MAX_DEVICE_NUM; ++i) {
        current_value = global_value_array[i];
        WriteUnit(current_value);
    }

    OutPutData();

    return 1;
}

static int ad5420_ioctl(struct inode *inode, struct file *file,
                  unsigned int command, unsigned long args) {
    int     device_index_;
    struct  data_unit  current_unit;

    if (copy_from_user(&current_unit,(const char *)args,sizeof(current_unit)) 
        != 0) {
        printk("copy_from_user failed \n");
        return 0;
    }

    device_index_ =  current_unit.device_index;
    if ((device_index_ < 0) || (device_index_ > (MAX_DEVICE_NUM - 1))) {
        printk("device index should between [0,5] \n");
        return 0;
    }

    global_value_array[device_index_] = current_unit.out_value;

    return 1;
}

// learn the write mode timing diagram from the datasheet
void ShiftCommand(unsigned char command) {
    int i;
    unsigned char command_;
    unsigned char command_flag;

    command_ = command;
    for(i = 0; i < CMD_BIT_NUM; ++i) {
        command_flag = command_ &  0x80;
        if (0 == command_flag)
            gpio_set_value(SDIN_PIN ,0);                 
        else
            gpio_set_value(SDIN_PIN ,1);                 
        ndelay(50000);

        gpio_set_value(SCLK_PIN ,1);    
        ndelay(50000);

        gpio_set_value(SCLK_PIN ,0);    
        ndelay(50000);

        command_   =   command_ << 1;
    }
}

void ShiftData(unsigned int data) {
    int i;
    unsigned int data_;
    unsigned int data_flag;

    data_ = data;
    for(i = 0; i < DATA_BIT_NUM; ++i) {
        data_flag = data_ & 0x8000;
        if (0 == data_flag)
            gpio_set_value(SDIN_PIN ,0);                 
        else
            gpio_set_value(SDIN_PIN ,1);                 
        ndelay(50000);

        gpio_set_value(SCLK_PIN ,1);    
        ndelay(50000);

        gpio_set_value(SCLK_PIN ,0);    
        ndelay(50000);

        data_ = data_ << 1;
    }
}

void OutPutData() {
    gpio_set_value(SDIN_PIN, 0);   
    gpio_set_value(SCLK_PIN ,0);   
    gpio_set_value(LATCH_PIN,1);   
    ndelay(50000);
    gpio_set_value(LATCH_PIN,0);   
}

void   InitDevice(void) {
    int i;
    unsigned char command;
    unsigned int  data;

    // reset, see the chip's datasheet
    for(i = 0; i < MAX_DEVICE_NUM; ++i) {
        gpio_set_value(LATCH_PIN,0);   
        gpio_set_value(SCLK_PIN ,0);   
        ndelay(50000);

        command = 0x56;   
        ShiftCommand(command);

        data = 0x0001;
        ShiftData(data);

        // nop
        command =  0x00; 
        ShiftCommand(command);

        data = 0x0000;  
        ShiftData(data);

        OutPutData();

        ndelay(1000000);
    }
}

void   WriteUnit(unsigned int out_value) {
    unsigned char command;
    command = 0x01;
    ShiftCommand(command);
    ShiftData(out_value);
}

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("AD5420 driver");
MODULE_AUTHOR("hlding");

[/pre]
In order to use our device driver, we should run these two commands:


insmod AD5420.ko
mknod /dev/myAD5420 c 253 0

And here is our tiny test program:

[pre]

#include    <stdio.h>
#include    <fcntl.h>
#include    <unistd.h>
#include    <sys/ioctl.h>
#include 	<linux/types.h>

#define MAX_DEVICE_NUM 6 // 6 AD5420 chips in the daisy-chain mode
#define DEVICE_NAME "/dev/myAD5420" // the name of the device
#define SET_CHANNEL_DATA 1

struct data_unit{
    unsigned char device_index; // between [0,MAX_DEVICE_NUM-1]
    unsigned int out_value;     // 12 bits valid
};

unsigned int test_value_array[MAX_DEVICE_NUM]= {0x7f,0x7f,0x7f,0x7f,0x7f,0x7f};

int main(int argc, char *argv[])
{
    int i, ret;
    struct data_unit current_unit;
    int device_handle = -1;

    // open the device handle
    device_handle = open(DEVICE_NAME, O_RDWR, 0);
    if (device_handle < 0) {
        printf("open the device failed!\n");
        return -1;
    }

    // prepare data
    for(i = 0; i < MAX_DEVICE_NUM-1; ++i) {
        current_unit.device_index = i;
        current_unit.out_value = test_value_array[i];
        ret = ioctl(device_handle, SET_CHANNEL_DATA, &current_unit);
        if (0 == ret) {
            printf("ioctl failed!\n");
            close(device_handle);
            return -1;
        }
    }

    // output
    ret = write(device_handle, NULL, NULL);

    // close the device handle
    close(device_handle);

    return 0;
}

[/pre]
 

References

OMAP-L138 C6000 DSP+ARM Processor Technical Reference Manual

Click to access spruh77b.pdf

AD5410_5420 data sheet

Click to access AD5410_5420.pdf

OMAP-L138 Software Design Guide
http://processors.wiki.ti.com/index.php/OMAP-L138_Software_Design_Guide#Linux_Drivers:

Linux Kernel and Driver Development Training

Click to access linux-kernel-slides.pdf

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s