Menu Close

Device Tree Driver Code Breakdown

This article breaks down and explains the device tree driver code.

For related subjects, please refer to the SOC Table of Contents.

 

First, we include the packages and libraries to be used.

/*  fii-dt-driver.c - The simplest kernel module.
* Copyright (C) 2013 - 2016 Xilinx, Inc
*
*   This program is free software; you can redistribute it and/or modify
*   it unde r the terms of the GNU General Public License as published by
*   the Free Software Foundation; either version 2 of the License, or
*   (at your option) any later version.

*   This program is distributed in the hope that it will be useful,
*   but WITHOUT ANY WARRANTY; without even the implied warranty of
*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*   GNU General Public License for more details.
*
*   You should have received a copy of the GNU General Public License along
*   with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/uaccess.h>  /* for put_user */

#include <linux/of_address.h>
#include <linux/of_device.h>
#include <linux/of_platform.h>

linux/kernel.h shows that we are doing kernel work, and allows for the printk() command to be used, which prints messages to the kernel log. printk() is used mainly as a logging mechanism for the kernel, which logs information and gives warnings.

linux/init.h allows for the usage of the module_init() and module_exit() macros.

linux/module.h shows that we are specifically working on a module with regard to kernel work. Modules are code segments that can be loaded and unloaded to the kernel on demand.

linux/slab.h helps to allocate arrays of elements.

linux/io.h is a C wrapper for input/output interfaces and has macros for many other useful functions.

linux/interrupt.h allows for the device to interrupt the CPU to stop what it is doing and respond to the device.

linux/uaccess.h allows for the usage of the get_user and put_user commands. This includes the copy_to_user command and the copy_from_user command.

linux/of_address.h includes several other packages that deals with addresses.

linux/of_device.h adds platform_device.h and of_platform.h.

linux/of_platform.h helps deal with platform drivers and registration.

 

/* Standard module information, edit as appropriate */

MODULE_LICENSE("GPL");
MODULE_AUTHOR
    ("FII inc.");
MODULE_DESCRIPTION
    ("fii-dt-driver - loadable module template generated by petalinux-create -t modules");
#define DRIVER_NAME "fii-dt-driver"

MODULE_LICENSE(“GPL”) is used to suppress the warning saying the code is not open source.

MODULE_AUTHOR() is used to declare the module’s author.

MODULE_DESCRIPTION() is used to describe what the module does.

#define DRIVER_NAME “fii-dt-driver” defines the driver’s name.

 

static const struct of_device_id of_match_fii_dt_driver[] = {
//    {.compatible = "fii,fii-dt-driver", .data = NULL},
    {.compatible = "fii,fii-dt-driver", },
    {/*sentinel*/},
};

//platform driver

static struct class  *fii_dt_driver_class;
static unsigned int major;
static unsigned int * gpio_base;
static unsigned int * gpio_data;
static unsigned int * gpio_dir;

static int fii_dt_driver_init(void);
static int fii_dt_driver_open(struct inode* node,struct file* filp){

    gpio_data = ioremap(gpio_base, 8);

    if(gpio_data){
        printk("kernel: ioremap(0x%08x)=0x%08x \n", gpio_base, gpio_data);
    }

    else{
        return -EINVAL;
    }

    gpio_dir = gpio_data + 1;

    return 0;
}

ioremap maps the physical address of an input/output device to the kernel virtual address. Where the input is (physical_address, size). We can see that the physical address is gpio_base, and the size is 8.

*Note that Linux remaps the physical addresses of devices for easier management.

The printk statement helps log the data and directory.

Because gpio_data is a pointer to an address, it will mean it loaded incorrectly if it has a value of 0. Thus, it will proceed to throw -EINVAL.

Note that -EINVAL is a macro for “Invalid Argument”.

 

static int fii_dt_driver_release(struct platform_device * dev)
{
    iounmap(gpio_data);
    unregister_chrdev(major, "fii-dt-driver");
    device_destroy(fii_dt_driver_class,MKDEV(major,0));
    class_destroy(fii_dt_driver_class);   

    return 0;
}

Here, the driver exit function is defined above. The device is removed from the system via unregister_chrdev and device_destroy. While class_destroy destroys the struct class structure along with pointers.

 

/*
    0x43c0_0000 + 4 = value
    0x43c0_0000 + 8 = direction
*/
static ssize_t fii_dt_driver_read(struct file *filp, char *buf, size_t size, loff_t *offset)
{
    unsigned char val[32];
    unsigned int temp;
    unsigned int *addr_p;
    int i,cnt;
    printk("kernel: fii-dt-driver read start.... size = %d\n", size);

    cnt = size;
    addr_p = gpio_data;
    temp = *addr_p ;

    for(i = 0; i < cnt/4; i++ )
    {
        val[i*4 + 3] = temp & 0xff;
        val[i*4 + 2] = (temp >> 8) & 0xff;
        val[i*4 + 1] = (temp >> 16) & 0xff;
        val[i*4 + 0] = (temp >> 24) & 0xff;

        addr_p ++;
        temp = *addr_p;
    }

    if(i % 4 == 1)
    {
        val[i*4 + 0] = temp & 0xff;
    }

    if(i % 4 == 2)
    {
        val[i*4 + 1] = temp & 0xff;
        val[i*4 + 0] = (temp >> 8) & 0xff;
    }

    if(i % 4 == 3)
    {
        val[i*4 + 2] = temp & 0xff;
        val[i*4 + 1] = (temp >> 8) & 0xff;
        val[i*4 + 0] = (temp >> 16) & 0xff;
    }

    copy_to_user(buf, val, cnt);

    return cnt;
}

The above section serves to read from the device. It is taking the information stored in temp, which is the address of gpio_data, and storing it in the val array. Note that the copy_to_user function at the end has the parameters (destination, source, size). Where the function will copy size number of bytes pointed at by source, which much exist in kernel-space, to destination, which must exist in user-space.

The main part of this section is lines 100 to 126. It breaks up the data into 8-bit chunks, and stores the chunks in reverse order, which allows for it to be in normal order when it is eventually read. Note that & 0xff serves as a mask which only leaves the right-most 8 bits. If the size is not a multiple of 4, it will simply append the extras to the end.

 

static ssize_t fii_dt_driver_write(struct file * filp, const char __user *buf, size_t size, loff_t * offset) {
    unsigned char val[32];
    unsigned int temp = 0;
    unsigned int * addr_p;
    int i,cnt;

    memset(val,0,32);
    addr_p = gpio_data;

    printk("kernel: fii-dt-driver write start.... size = %d\n", size);

    copy_from_user(&val, buf, size);

    cnt = size - 1;

    printk("kernel: val[0] = 0x%08x \n", val[0]);

    if(val[0] == 'w')
    {
        temp = val[2];
        temp = temp << 8 | val[3];
        temp = temp << 8 | val[4];
        temp = temp << 8 | val[5];

        *addr_p = temp;

        printk("kernel: gpio_data = 0x%08x \n", temp);
    }

    else if(val[0] == 'd')
    {
        addr_p ++;
        temp = val[2];
        temp = temp << 8 | val[3];
        temp = temp << 8 | val[4];
        temp = temp << 8 | val[5];

        *addr_p = temp;

        printk("kernel: gpio_dir = 0x%08x \n", temp);
    }

    else
    {
        printk("kernel: invalid parameter \n");
    }

    return size;

}

The above section focuses on writing to the device. Making use of the copy_from_user command, which also has the parameters (destination, source, size). Where the function will copy size number of bytes pointed at by source, which much exist in user-space, to destination, which must exist in kernel-space.

The section also deals with taking in the inputs from the application, in the case of the ‘dir’ ‘wr’ and ‘rd’ commands. It takes in the first character to determine which command it is and prepares the data, as well as writes data to the log file accordingly.

 

static struct file_operations fii_dt_driver_oprs = {
    .owner      = THIS_MODULE,
    .open       = fii_dt_driver_open,
    .write      = fii_dt_driver_write,
    .read       = fii_dt_driver_read,
};

The above section deals with the file_operations structure, which is defined in linux/fs.h, and it holds various pointers to functions defined by the driver in order to perform operations on the device.

 

static int fii_dt_driver_probe(struct platform_device *pdev)
{
    struct resource *res;
    printk ("kernel: enter fii_dt_driver probe ...... \n");
    printk ("kernel: enter fii_dt_driver probe ...... \n");
    printk ("kernel: enter fii_dt_driver probe ...... \n");

    res = platform_get_resource(pdev, IORESOURCE_MEM,0);

    if(res){
        gpio_base = res->start;
    }

    major=register_chrdev(0, "fii-dt-driver", &fii_dt_driver_oprs);

    if (major < 0) {
        printk ("Registering the character device failed with %d\n", major);

        return major;
    }

    fii_dt_driver_class = class_create(THIS_MODULE, "fii-dt-driver_class");
    device_create(fii_dt_driver_class,NULL,MKDEV(major,0),NULL,"fii-dt-driver");

    return 0;
}

The above section deals with driver entry functions. The three lines of printk helps the user identify if it booted successfully, as the user will see the three lines displayed in the console. Char devices are accessed through device files, which is typically located in the /dev folder.

Registering the device with the kernel adds the driver to the system. Note that this is done using the register_chrdev function, which is defined in linux/fs.h.

Where unsigned int major is the major number to be requested, const char *name is the name of the device which will appear in /proc/devices and struct file_operations *fops is a pointer to the file_operations table for the driver. Note that if the function returns a negative value, it means that the registration failed.

 

MODULE_DEVICE_TABLE(of, of_match_fii_dt_driver);

static struct platform_driver fii_dt_driver_drv = {
    .driver     = {
        .name   = "fii-dt-driver",
        .owner  = THIS_MODULE,
        .of_match_table = of_match_fii_dt_driver,
    },

    .probe      = fii_dt_driver_probe,
    .remove     = fii_dt_driver_release,
};

The above section defines the module device table, and deals with the platform_driver structure, which is defined in, and it holds various pointers to functions defined by the driver in order to perform operations on the device.

 

static int fii_dt_driver_init(void){       /* register device  */
    return platform_driver_register(&fii_dt_driver_drv);
}

static void fii_dt_driver_exit(void){
    platform_driver_unregister(&fii_dt_driver_drv);
    return;
};

//module_platform_driver(fii_dt_driver_drv);

module_init(fii_dt_driver_init);
module_exit(fii_dt_driver_exit);

MODULE_ALIAS("platform:fii-dt-driver");

The above section defines functions that register and deregister the device.

module_init() defines the function to be called during module insertion or upon boot. In this case the function is swled_init.

module_exit() defines the function to be called during module removal. Everything must be cleaned up when it returns. In this case the function is swled_exit.

MODULE_ALIAS is a macro used to give a special name directly in the module source. In this case it is “platform:fii-dt-driver”.

Posted in Textbook and Training Project

Related Articles

Leave a Reply

Your email address will not be published.

Leave the field below empty!