Menu Close

简单字符设备驱动代码解析

此文章解析了上一篇文章中简单字符设备驱动的代码。

相关参考文章:SOC 教学教案

 

首先我们将添加我们将使用的组件。

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/errno.h>
#include <linux/io.h>
#include <linux/of.h>
#include <linux/of_device.h>

linux/kernel.h 表明我们在做内核工作,使我们能够使用 printk() 命令往内核 log 文件中添加信息。

linux/module.h 表明我们在做与内核有关的模块工作。模块是可以根据需求在内核装卸的代码段。

linux/fs.h 通过内核定义设备的文件结构,比如后面的第 148 行使用 register_chrdev 命令把驱动添加到系统中。

linux/uaccess.h 使我们能够使用 get_user 和 put_user 命令。这包括第 86 行的 copy_to_user 命令以及第 101 行的 copy_from_user 命令。

asm/errno.h 在编译 Linux 内核时十分关键。

linux/io.h 和 linux/of.h 是输入输出界面的 C 包装器,并包含其他十分有用的函数。

linux/of_device.h 帮助处理平台驱动以及注册工作。

 

/*
    0x43c0_0000 + 4 = value
    0x43c0_0000 + 8 = direction
*/

#define DEVICE_NAME   "fii-module"
#define CLASS_NAME    "fii-module_class"

static int major;
static struct class  *swled_class;

static  unsigned int *gpio_data;
static  unsigned int *gpio_dir ;

以上代码做一些基本的定义。

 

static int swled_open(struct inode *node,struct file *filp){

    unsigned int gpio_base = (0x43c00000);

    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;

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

    return 0;
}

swled_open 函数控制缓冲内存的一部分。这一部分用于访问设备。

ioremap 将输入/输出设备的物理地址转换为内核中的虚拟地址。其中输入的参数为 (physical_address, size),物理地址和大小。我们可以看到它的物理地址是 gpio_base,并且大小为 8。注意 Linux 下这样转换地址使其在 Linux 中更容易管理。

因为 gpio_data 指向一个地址,所以如果它的值为 0 那将表示没有正确加载。因此它将返回 -EINVAL,表示 “Invalid Argument”。

接下来的两行 printk 帮助记载数据和运行过程。

 

int swled_release(struct inode *inode, struct file *filp)
{
//  MOD_DEC_USE_COUNT;
    iounmap(gpio_data);

    return 0;
}

以上代码用于关闭并推出设备。iounmap 与 ioremap 相反。ioremap 用于转换物理地址,而 iounmap 用于将其摧毁。

 

static ssize_t swled_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 = 0, cnt;
    printk("kernel: swled_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;
}

以上代码用于从设备读取信息。把 temp 中存取的 gpio_data 考到 val 数组中。注意最下方 copy_to_user 函数的参数为(destination, source, size);分别为目的地址,原始地址,以及大小(字节)。此函数将把原始地址大小数量的字节拷贝到目的地址。注意原始地址必须存在于内核空间,并且目的地址需要存在于用户空间。

这一段代码的主要部分为第 60 行与 84 行之间。它把数据分成 8 字节的小段,并且将其以反向顺序保存;使其在被读取的时候是正确的顺序。注意 & 0xff 用于掩盖数据,只留下最右边的 8 个字节。如果数据大小不是 4 的倍数,它将把多余的在最后附上。

 

static ssize_t swled_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: swled_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;
}

以上代码用于将数据写到设备上,主要使用 copy_from_user 函数。它的参数也是 (destination, source, size)。此函数将把原始地址大小数量的字节拷贝到目的地址。注意原始地址必须存在于用户空间,并且目的地址需要存在于内核空间。

这一部分也处理应用的输入,例如用户输入的 ‘dir’ ‘wr’ 以及 ‘rd’。它将通过查看第一个字符判断用户的输入并准备相应的数据与操作,同时也往 log 文件中添加信息。

 

static struct file_operations   swled_oprs = {
    .owner  = THIS_MODULE,
    .open   = swled_open,
    .write  = swled_write,
    .read   = swled_read,
    .release= swled_release,
};

以上部分用于处理 linux/fs.h 中定义的 file_operations 结构体。它包含指向驱动程序定义的函数的指针,以便在设备上执行操作。

 

static int swled_init(void){       /* register device  */
/*
 *
 * int register_chrdev (unsigned int major,
 *                      const char *name,
 *                      struct file_operations *fops);
 *
 *  major => 0, automatic mode; others major device ID
 */

    major=register_chrdev(0, DEVICE_NAME, &swled_oprs);

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

    swled_class = class_create(THIS_MODULE, CLASS_NAME);
    device_create(swled_class, NULL, MKDEV(major,0), NULL, DEVICE_NAME);

    return 0;
}

以上部分用于处理驱动入口函数。字符设备通过设备文件访问,通常位于 /dev 目录下。

设备与内核注册后通过 linux/fs.h 下的 register_chrdev 把驱动添加到系统中。

其中 register_chrdev 的参数分别是请求的 major 数,将出现在 /proc/devices 里的设备名,以及驱动指向 file_operations 列表的指针。注意如果此函数返回一个负数,那将表示注册失败。

 

static void swled_exit(void){
    unregister_chrdev(major, DEVICE_NAME);
    device_destroy(swled_class,MKDEV(major,0));
    class_destroy(swled_class);
};

module_init(swled_init);
module_exit(swled_exit);

MODULE_LICENSE("GPL");

这里定义了驱动出口函数,用于注销字符设备驱动。设备通过 unregister_chrdev 和 device_destroy 从系统中去除。最后 class_destroy 摧毁 struct 结构以及指针。

module_init() 定义了模块插入以及启动时运行的函数。在这里函数是 swled_init。

module_exit() 定义了模块去除时运行的函数。在返回时所有东西需要已经被清除。在这里函数是 swled_exit。

MODULE_LICENSE(“GPL”) 用于压制警告说代码不是开源的。

Posted in 教材与教案

发表评论

相关链接