学习字符设备驱动

说明:
开发环境:ubuntu14.04
硬件环境:EasyArm-i.mx283A

常识先了解

打开一个文件时系统调用的过程

学习字符设备驱动_第1张图片

 Linux下设备以文件的形式呈现,应用程序对设备的操作就像操作一般的文件IO一样,open,write、read、close,这样做的好处就是为上层应用提供了统一的编程接口,操作简单易行。对于字符设备来说,设备号、cdev、操作方法集合至关重要,内核找到路径名所对应的inode后,要和驱动建立连接,首先要做的就是根据inode中的设备号找到cdev,然后根据cdev找到关联的操作方法集合,从而调动驱动所提供的操作方法完成对设备的具体操作。简而言之,字符设备驱动框架就是围绕着设备号、cdev和操作方法集合来实现的。cdev是放在在cdv_map散列表中的一个结构体,驱动加载时构造除cdev,并添加到cdev_map散列表中,构造cdev的同时还实现了一个操作方法集合,cdev的成员ops指向它。

 设备号分为主设备号和次设备号,主设备号用于区分同一类设备,次设备号用于区分同一类设备的不同个体,比如在一个开发设备上装有led1、led2、led3、led4,则主设备号用于led这类设备,次设备号分别用于led1、led2、led3、led4。在32位机子下,设备号用32位来表示,其中高12位用于主设备号,低20位用于次设备号。驱动开发中可以使用静态分配和动态分配两种方法分配设备号,静态分配可能会出现设备号冲突的情况,导致模块加载失败,动态分配不会出现这种情况,但是动态分配无法从程序中预先配置设备号,设备号只能在/proc/devices下查看。

字符设备驱动框架

#include 
#include 
#include 
#include 
#include 

#define CH_MAJOR 256
#define CH_MINOR 0
#define CH_DEV_CNT 1
#define CH_DEV_NAME "CHDEV"




static struct cdev chdev_s;//定义一个字符设备结构体

//定义一个字符设备操作集合
static struct file_operations chdev_ops={

    .owner = THIS_MODULE,


};


/*模块初始化*/
static int __init chdev_init(void)
{

    int ret;
    dev_t dev;

    //静态分配设备号
    dev = MKDEV(CH_MAJOR,CH_MINOR);//将主次设备号合并成一个设备号
    ret = register_chrdev_region(dev,CH_DEV_CNT,CH_DEV_NAME);//静态注册一个设备号

    if(ret)
    {
        goto reg_err;
    }

    cdev_init(&chdev_s,&chdev_ops);
    chdev_s.owner = THIS_MODULE;

    ret = cdev_add(&chdev_s,dev,CH_DEV_CNT);
    if(ret)
    {
        goto add_err;
    }

    return 0;

add_err:
    unregister_chrdev_region(dev,CH_DEV_CNT);

reg_err:
    return ret;

}


/*模块退出*/
static void __exit chdev_exit(void)
{

    dev_t dev;
    dev = MKDEV(CH_MAJOR,CH_MINOR);

	cdev_del(&chdev_s);
    unregister_chrdev_region(dev,CH_DEV_CNT);//注销一个设备号

}




module_init(chdev_init);//定义模块初始化别名函数
module_exit(chdev_exit);//定义模块退出别名函数

//模块信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("YangZhengQing <[email protected]>");
MODULE_DESCRIPTION("Character device driver frame");

 在该驱动程序中采用静态分配的方式注册设备号,所以在程序中定义了主设备号为256,次设备号的起始号为0,设备个数为1个,表示次设备号从0开始注册一个设备。设备号一旦被注册,在没有卸载模块之前,该设备号会一直被占用,其他模块不允许再注册此设备号。如前所述,字符设备驱动围绕着设备号、cdev、操作方法集合来编写,chdev_s是一个cdev类型的结构体变量,该结构体包含这类设备的所有属性,比如设备号、操作方法集合、此设备的个数等,结构体如下:

struct cdev {
    struct kobject kobj;
    struct module *owner;//该字符设备所在的内核模块的对象指针.
    const struct file_operations *ops;//指向该字符设备的操作方法集合
    struct list_head list;//用来将已经向内核注册的所有字符设备形成链表
    dev_t dev;//设备号
    unsigned int count;//次设备号的个数
};

chdev_ops是一个file_operations类型的结构体变量,该结构体包含操作设备的方法的指针,比如open、write、read、close等,结构体如下:

struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
};

可以看到该结构体中的成员都是函数指针,说明后面编写的操作函数都要初始化到这个结构体中,上层应用对设备的具体操作才会生效。

 讲完字符设备驱动所需要的变量之后,就应该来讲讲如何使用和配置这些变量了。首先在chdev_init函数中通过宏MKDEV将主设备号和次设备号合并成一个设备号,然后通过register_chrdev_region函数静态注册这个设备号,该函数的声明如下:

int register_chrdev_region(dev_t, unsigned, const char *);

第一个表示设备号,第二个参数表示次设备的个数,第三个参数表示该设备的名字,通过这个名字,当insmod加载成功模块后,cat/proc/devices可以查看到相应的设备号和设备名。至此,设备号的初始化就已完成,后面就是初始化cdev的过程。

 函数cdev_init作用就是初始化cdev的部分成员,其中最重要的就是将ops指针指向file_operations类型的chdev_ops操作方法集合,这样就可以通过设备号找到操作方法集合,进行相应的操作。函数的声明如下:

void cdev_init(struct cdev *cdev,const struct file_operations *fops);

第一个参数传入cdev的地址,第二个参数传入file_operations的地址。函数内部实现将两者关联起来,手牵手,一起走向未来!

chdev_s.owner = THIS_MODULE意思是说通过chdev_s找到对应的模块。owner是一个指向struct module类型变量的指针,THIS_MODULE是包含驱动的模块中struct module类型对象的地址,相当于c++中的this指针。

 cdev初始化完成之后,应该将cdev添加到内核的cdev_map散列表中,内部生成一个probe的结构对象,其中data成员指向cdev。散列表相当于哈希表,调用的函数是:

int cdev_add(struct cdev *, dev_t, unsigned);

同时,既然有添加函数,自然有删除函数cdev_del,在卸载模块的函数中就有用到。

 简而言之,字符驱动的框架其实很简单,主要用到以下的一些函数:

  • MKDEV():整合设备号
  • register_chrdev_region():注册设备号
  • cdev_init:初始化cdev
  • cdev_add:添加cdev到cdev_map中。
  • cdev_del:删除cdev
  • unregister_chrdev_region():取消设备号

Makefile文件


ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)

KERNELDIR := /home/yzq/EasyArm-imx28xx/kernel/linux-2.6.35.3

ROOTFS := /nfsroot/rootfs

else

KERNELDIR := /lib/modules/$(shell uname -r)/build

endif


PWD := $(shell pwd)

modules:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
    rm -rf *.ko *.order *.symvers *.cmd *.o *.mod.c *.tmp_versions .*.cmd .tmp_versions
else

obj-m := chdev.o

endif

这个Makefile实现了两种架构的编译模式,一种是编译成ubuntu平台的驱动模块,并将模块安装到/lib/modules/4.4.0-31-generic/extra目录下,具体操作如下:

  • make
  • make modules_install

另一种是编译成arm平台的驱动模块,并将模块安装到/nfsroot/rootfs/lib/modules/2.6.35.3-571-gcca29a0/extra下,nfsroot是通过NFS服务器搭建的一个网络共享文件,开发板远程启动该目录下的rootfs根文件系统,将模块安装到该rootfs下的之后,可以直接登陆开发板进行安装操作。具体操作如下:

  • make ARCH=arm
  • make ARCH=ARM modules_install

测试字符驱动
模块加载完成后,第一步cat /proc/devices查看设备号,第二步mknod 安装设备节点,第三步cat /dev/CHDEV查看是否驱动是否正常运行。
在这里插入图片描述
没有加载模块之前,cat查看字符设备节点显示没有该设备。

学习字符设备驱动_第2张图片
加载设备之后,可以看到字符设备的设备号和名字。

学习字符设备驱动_第3张图片

可以看到显示读取错误,无效的参数。此时可以判断驱动正在运行,只不过在驱动编写时并没有实现具体读取设备的操作方法。

你可能感兴趣的:(linux,驱动程序,嵌入式)