zoukankan      html  css  js  c++  java
  • 字符设备的驱动

    更新记录

    version status description date author
    V1.0 C Create Document 2018.12.26 John Wan
    V2.0 A 添加各设备注册函数说明 2019.3.20 John Wan
    V3.0 M 根据 《Linux设备驱动开发详解》进行了重新梳理 2019.4.23 John Wan

    status:
    C―― Create,
    A—— Add,
    M—— Modify,
    D—— Delete。

    注:内核版本 3.0.15

    一、驱动程序的开发概述

    1.1 应用程序、库、内核、驱动程序的关系

      从上到下,一个软件系统可以分为应用程序、库、操作系统(内核)、驱动程序。开发人员可以专注于自己熟悉的部分,对于相邻层,只需了解它的接口,无需关注内部实现细节。但需了解整体的运作逻辑,以及各层实现的功能。

      在 Linux 中一切皆文件,那么也是通过文件操作驱动。

      这4层的协作关系如图:

    图01 Linux 软件系统的层次关系

      (1) 应用程序使用库提供的 open、read、write、ioctl等接口函数进行操作(称为系统调用)(在 gcc 编译工具链 /libc/usr 目录下 fcntl.h、unistd.h、sys/ioctl.h等文件中找到 open、read、write、ioctl 函数原型)。

      (2) 而这些接口函数都是设置好相关寄存器的,调用时库就执行 “swi” 指令(ARM架构),不同的函数对应 “swi” 的不同参数,该指令会引起 CPU 异常,进入内核。

      (3) 内核的异常处理函数根据这些参数执行各种操作,比如根据设备文件名找到对应的驱动程序,调用驱动程序的相关函数等。一般来说,当应用程序调用 open、read、write等函数后,将会使用驱动程序中的 open、read、write函数来实现相关操作,比如初始化、读、写等。

    1.2 Linux 驱动程序的分类

      字符设备:是能够像字节流(如文件)一样被访问的设备,就是说对它的读写是以字节为单位的。比如串口在进行收发数据时就是一个字节一个字节进行传输的。字符设备的驱动程序中实现了open、close、read、write等系统调用,应用程序可以通过设备文件(如/dev/ttySAC0等)来访问字符设备。

      块设备:块设备上的数据以块的形式存放,比如 NAND Flash 上的数据就是以页为单位存放的。块设备驱动程序向用户层提供的接口与字符设备一样,应用程序也可以通过相应的设备文件(如/dev/mtdblock0、/dev/hda1 等)来调用 open、close、read、write等系统调用,与块设备传输任意字节的数据。对用户而言,字符设备和块设备的访问方式没有差别

      差别:

      (1) 由于块设备处理数据必须成块(如以页为单位进行擦除、读、写),因此在操作硬件的接口实现方式不一样。

      (2) 数据块上的数据可以有一定的格式,不同的文件系统类型就是用来定义这些格式的。(如硬盘的不同文件格式)

      网络设备:同时具有字符设备、块设备的部分特点。如果说它是字符设备,它的输入/输出却是有结构的、成块的(报文、包、帧);如果说它是块设备,它的“块”又不是固定大小的,大道数百甚至数千字节,小到几字节。库、内核提供了另一套和数据包传输相关的函数。

    1.3 Linux驱动程序开发步骤

      Linux 的内核是由各种驱动组成,对各种设备的支持非常完善,基本上可以找到同类设备,在其基础上进行小幅度修改以符合具体设备驱动。

      因此编写驱动程序的难点并不是硬件的具体操作,而是弄清楚现有驱动程序的框架,在这个框架中加入这个硬件。

      驱动开发的大致流程如下:

      (1) 查看原理图,数据手册,确定设备类型,了解设备的操作方法;

      (2) 在内核中找到相近的驱动程序,以它为模板进行开发,有时候需要从零开始;

      (3) 实现驱动程序的初始化:如向内核注册这个函数,这样应用程序传入文件名时,内核才能找到相应的驱动程序。

      (4) 设计所要实现的操作,如 open、close、read、write等;

      (5) 实现中断服务(根据需求添加),或其它功能;

      (6) 编译该驱动程序到内核总,或手动用命令加载;

      (7) 测试驱动程序。

    二、字符设备驱动

    2.1 cdev 字符设备驱动结构

      在 linux 内核中,使用 cdev 结构体描述一个字符设备:

    struct cdev {
    	struct kobject kobj;			/* 内嵌的 kobject 对象 */
    	struct module *owner;			/* 所属模块 */
    	const struct file_operations *ops;	/* 文件操作结构体 */
    	struct list_head list;			/**/
    	dev_t dev;						/* 设备号 */
    	unsigned int count;				/**/
    };
    

    2.1.1 file_operation

      对于每个系统调用函数,驱动中都有一个与之对应的函数。对于字符设备驱动程序,驱动中对应的函数集合在一个名为 file_operation 类型的数据结构中。原型在文件includelinuxfs.h 中:

    /*
     * NOTE:
     * all file operations except setlease can be called without
     * the big kernel lock held in all filesystems.
     */
    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 *);
    /* remove by cym 20130408 support for MT660.ko */
    #if 0
    //#ifdef CONFIG_SMM6260_MODEM
    #if 1// liang, Pixtree also need to use ioctl interface...
    	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    #endif
    #endif
    /* end remove */
    	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 **);
    	long (*fallocate)(struct file *file, int mode, loff_t offset,
    			  loff_t len);
    /* add by cym 20130408 support for MT6260 and Pixtree */
    #if defined(CONFIG_SMM6260_MODEM) || defined(CONFIG_USE_GPIO_AS_I2C)
    	int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    #endif
    /* end add */
    };
    

      编写字符设备驱动程序,其实就是为具体硬件的 file_operation 结构填充各函数

      那么具体的某个字符设备的驱动程序与内核之间是如何联系起来的?毕竟会有很多字符设备的驱动程序,而内核需要对这些进行区分。答:通过设备号,主/次设备号。

    2.1.2 dev_t dev 设备号

      设备驱动不仅有不同类型的划分,同类型设备驱动中还会用主/次设备号进一步划分。主设备号用来标识设备对应的驱动程序,告诉内核使用哪个驱动程序为该设备提供服务;而次设备号则用来标识该驱动程序下具体且唯一的某个设备。

       cdev 结构体的 dev_t 成员定义了设备号,为32位,其中高12位为主设备号,低20位为次设备号:原型在 /include/linux/kdev_t.h

    #define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))
    
    参数
    -ma		//主设备号
    -mi		//次设备号
    
    注意到,主、次设备号共同组成了4字节,高12位为主设备号,低20位为次设备号,获取主、次设备号通过:
    
    #define MINORBITS	20
    #define MINORMASK	((1U << MINORBITS) - 1)
    
    #define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
    #define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
    

      在系统中查看某驱动的设备号,例如:

    [root@iTOP-4412]# ls /dev/ttyS0 -l
    crw-rw----    1 root     0           4,  64 Aug 19 01:48 
    

      crw-rw---- 中的 c 标识字符设备,主设备号为4,次设备号为64。

      驱动是可以有唯一的标识,那么内核是如何知道主/次设备号与某个驱动是对应的呢?答:通过驱动注册,在驱动向内核注册的过程中,将设备号与该设备驱动进行绑定。

    2.2 字符设备驱动的注册/卸载

      向内核进行注册,就是告诉内核,将主设备号与设备驱动对应的 file_operation 绑定,从而建立起它们之间的联系。

      通过原型在文件includelinuxfs.h 中的 register_chrdev函数:

    static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
    {
    	return __register_chrdev(major, 0, 256, name, fops);
    }
    
    参数:
        -major		//主设备号,0~255,输入0表示由系统分配由函数返回,非零表示设定。
        -name		//字符设备驱动的名字,可通过lsmod查看
        -fops		//该字符设备驱动的file_operation数据结构
    

      当然在不需要这种联系时,可通过 unregister_chrdev 取消解除绑定关系。

    static inline void unregister_chrdev(unsigned int major, const char *name)
    {
    	__unregister_chrdev(major, 0, 256, name);
    }
    

    注:了解register_chrdev()、register_chrdev_region()、alloc_chrdev_region()功能及差异

    分配设备编号,注册设备与注销设备的函数均在fs.h中申明,如下:
    int register_chrdev_region(dev_t, unsigned, const char *); //静态的申请和注册设备号 
    int alloc_chrdev_region(dev_t, unsigned, const char *);    //动态的申请注册一个设备号
    int register_chrdev(unsigned int, const char *,struct file_operations *); //int为0时候动态注册,非零时候静态注册。
    
    int unregister_chrdev(unsigned int, const char *);   //注销设备号
    void unregister_chrdev_region(dev_t, unsigned);   //注销设备号
    

      前面已经了解到设备号的作用,以及申请方式,那么应用程序又是如何找到设备对应的驱动程序的呢?

      linux中一切皆文件,应用程序是通过操作文件的方式来进行控制的,例如open("xxx", O_RDWR),而在前面说明中,并没有出现生成文件的操作。设备号面向的是内核,而不是应用层。那么要如何给应用层提供接口?答:通过设备节点。

    2.3 设备节点的生成与注销

      通过设备号可以精确的定位到设备对应的驱动程序,那么给应用层提供接口也是基于设备号来定位具体操作的设备。只不过,对于应用层来说,是以文件的方式进行操作,那么就需要将设备号与文件联系起来,这指的就是设备节点。可通过两种方式生成设备节点:

    • 手动

      使用 mknode 命令创建,原型:

    mknod Name { b | c } Major Minor
    
    参数:
    - Name		//创建的文件名,设备节点名称
    - { b | c }	//类型,b表示块设备,c表示字符设备
    - Major		//主设备号
    - Minor		//次设备号
    
    • 自动

      依赖于用户空间移植了 udev

      (1) 利用class_create()函数,根据设备驱动名字创建一个class类

    /include/linux/device/h
    
    /* This is a #define to keep the compiler from merging different
     * instances of the __key variable */
    #define class_create(owner, name)		
    ({						
    	static struct lock_class_key __key;	
    	__class_create(owner, name, &__key);	
    })
    
    参数:
    -owner		//所属,一般为THIS_MODULE
    -name		//注册主设备号时的驱动名称
    
    

      (2) 通过device_create()函数,为每个设备创建设备节点:

    struct device *device_create(struct class *class, struct device *parent,
    			     dev_t devt, void *drvdata, const char *fmt, ...)
    {
    	va_list vargs;
    	struct device *dev;
    
    	va_start(vargs, fmt);
    	dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
    	va_end(vargs);
    	return dev;
    }
    
    参数:
    -class		//设备驱动的类
    -parent		//父类,NULL
    -devt		//设备号,包括主设备号,次设备号,从次设备号申请了解到主设备号是偏移后或上次设备号
    -drvdata	//数据,NULL
    -fmt		//设备节点的名称
    
    

      加载好的模块可以在 /dev 目录下看到创建的设备节点

      可通过 device_unregister()函数删除设备节点,通过class_destroy()函数释放掉申请的class类

    2.4 cdev 相关操作函数

    void cdev_init(struct cdev *, const struct file_operations *);
    struct cdev *cdev_alloc(void);
    void cdev_put(struct cdev *p);
    int cdev_add(struct cdev *, dev_t, unsigned);
    void cdev_del(struct cdev *);
    
    

    cdev_init():初始化 cdev 成员,并建立 cdevfile_operation 之间的连接;

    cdev_alloc():动态申请一个 cdev 内存;

    cdev_add()、cdev_del():分别向系统添加和删除一个 cdev,完成字符设备的注册和注销。

      对 cdev_add() 的调用通常发生在字符设备驱动模块加载函数中,相反的,cdev_del()函数的调用通常发生在字符设备驱动模块卸载函数中。

      字符设备驱动的结构整体如下图所示:

      以上就是驱动与内核、应用层之间的连接。那么问题来了,这些的源头也就是驱动的注册是在什么时候开始运行的呢?答:在加载驱动的时候运行。

    2.4 模块的加载与卸载

      在驱动程序中引入 module_init()函数 与 module_exit() 函数,在模块进行加载时执行module_init()函数,卸载时执行module_exit() 函数,例如:

    module_init(leds_init);		//leds_init 执行的初始化函数
    module_exit(leds_exit);		//
    
    

      驱动模块的加载与卸载可通过手动或自动的方式来进行。

    2.4.1 手动加载:通过命令的方式

    命令:
    insmod 文件名		//加载,文件名的后缀 ".KO"
    rmmod 文件名		//卸载,
    lsmod			  //查看加载的模块
    cat /proc/devices	//查看运行中的模块
    
    

      insmodrmmod 命令是如何来控制驱动程序的呢?

      在驱动程序中引入 module_init()函数 与 module_exit() 函数,当执行insmod 命令时,就会调用 module_init() 函数。执行 rmmod 命令时,调用 module_exit() 函数。

      这样,前面各种需要注册、申请的情况都可以放在一个初始化函数中,然后通过 module_init(初始化函数名) 来调用。

    问题:

      在使用 rmmod 命令时会可能出现 "rmmod: can't change directory to '/lib/modules': No such file or directory" 这个错误。

      那么按照提示在 /lib 目录下建立对应的文件夹就行。

    2.4.2 自动加载

    1)编译进内核

      通过在对内核进行配置、编译时,将模块加载,具体的操作方式,另行总结。

    2)自动加载模块

      Linux启动自动加载模块

    三、demo

    /*
     * a simple char device driver: globalmem without mutex
     *
     * Copyright (C) 2014 Barry Song  (baohua@kernel.org)
     *
     * Licensed under GPLv2 or later.
     */
    
    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/init.h>
    #include <linux/cdev.h>
    #include <linux/slab.h>
    #include <linux/uaccess.h>
    
    #define GLOBALMEM_SIZE	0x1000
    #define MEM_CLEAR 0x1
    #define GLOBALMEM_MAJOR 230
    
    static int globalmem_major = GLOBALMEM_MAJOR;
    module_param(globalmem_major, int, S_IRUGO);
    
    struct globalmem_dev {
    	struct cdev cdev;
    	unsigned char mem[GLOBALMEM_SIZE];
    };
    
    struct globalmem_dev *globalmem_devp;
    
    static int globalmem_open(struct inode *inode, struct file *filp)
    {
    	filp->private_data = globalmem_devp;
    	return 0;
    }
    
    static int globalmem_release(struct inode *inode, struct file *filp)
    {
    	return 0;
    }
    
    static long globalmem_ioctl(struct file *filp, unsigned int cmd,
    			    unsigned long arg)
    {
    	struct globalmem_dev *dev = filp->private_data;
    
    	switch (cmd) {
    	case MEM_CLEAR:
    		memset(dev->mem, 0, GLOBALMEM_SIZE);
    		printk(KERN_INFO "globalmem is set to zero
    ");
    		break;
    
    	default:
    		return -EINVAL;
    	}
    
    	return 0;
    }
    
    static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size,
    			      loff_t * ppos)
    {
    	unsigned long p = *ppos;
    	unsigned int count = size;
    	int ret = 0;
    	struct globalmem_dev *dev = filp->private_data;
    
    	if (p >= GLOBALMEM_SIZE)
    		return 0;
    	if (count > GLOBALMEM_SIZE - p)
    		count = GLOBALMEM_SIZE - p;
    
    	if (copy_to_user(buf, dev->mem + p, count)) {
    		ret = -EFAULT;
    	} else {
    		*ppos += count;
    		ret = count;
    
    		printk(KERN_INFO "read %u bytes(s) from %lu
    ", count, p);
    	}
    
    	return ret;
    }
    
    static ssize_t globalmem_write(struct file *filp, const char __user * buf,
    			       size_t size, loff_t * ppos)
    {
    	unsigned long p = *ppos;
    	unsigned int count = size;
    	int ret = 0;
    	struct globalmem_dev *dev = filp->private_data;
    
    	if (p >= GLOBALMEM_SIZE)
    		return 0;
    	if (count > GLOBALMEM_SIZE - p)
    		count = GLOBALMEM_SIZE - p;
    
    	if (copy_from_user(dev->mem + p, buf, count))
    		ret = -EFAULT;
    	else {
    		*ppos += count;
    		ret = count;
    
    		printk(KERN_INFO "written %u bytes(s) from %lu
    ", count, p);
    	}
    
    	return ret;
    }
    
    static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
    {
    	loff_t ret = 0;
    	switch (orig) {
    	case 0:
    		if (offset < 0) {
    			ret = -EINVAL;
    			break;
    		}
    		if ((unsigned int)offset > GLOBALMEM_SIZE) {
    			ret = -EINVAL;
    			break;
    		}
    		filp->f_pos = (unsigned int)offset;
    		ret = filp->f_pos;
    		break;
    	case 1:
    		if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
    			ret = -EINVAL;
    			break;
    		}
    		if ((filp->f_pos + offset) < 0) {
    			ret = -EINVAL;
    			break;
    		}
    		filp->f_pos += offset;
    		ret = filp->f_pos;
    		break;
    	default:
    		ret = -EINVAL;
    		break;
    	}
    	return ret;
    }
    
    static const struct file_operations globalmem_fops = {
    	.owner = THIS_MODULE,
    	.llseek = globalmem_llseek,
    	.read = globalmem_read,
    	.write = globalmem_write,
    	.unlocked_ioctl = globalmem_ioctl,
    	.open = globalmem_open,
    	.release = globalmem_release,
    };
    
    static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
    {
    	int err, devno = MKDEV(globalmem_major, index);
    
    	cdev_init(&dev->cdev, &globalmem_fops);
    	dev->cdev.owner = THIS_MODULE;
    	err = cdev_add(&dev->cdev, devno, 1);
    	if (err)
    		printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
    }
    
    static int __init globalmem_init(void)
    {
    	int ret;
    	dev_t devno = MKDEV(globalmem_major, 0);
    
    	if (globalmem_major)
    		ret = register_chrdev_region(devno, 1, "globalmem");
    	else {
    		ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
    		globalmem_major = MAJOR(devno);
    	}
    	if (ret < 0)
    		return ret;
    
    	globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
    	if (!globalmem_devp) {
    		ret = -ENOMEM;
    		goto fail_malloc;
    	}
    
    	globalmem_setup_cdev(globalmem_devp, 0);
    	return 0;
    
     fail_malloc:
    	unregister_chrdev_region(devno, 1);
    	return ret;
    }
    module_init(globalmem_init);
    
    static void __exit globalmem_exit(void)
    {
    	cdev_del(&globalmem_devp->cdev);
    	kfree(globalmem_devp);
    	unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
    }
    module_exit(globalmem_exit);
    
    MODULE_AUTHOR("Barry Song <baohua@kernel.org>");
    MODULE_LICENSE("GPL v2");
    
    

    四、案例:LED

    硬件:迅为iTop4412精英板。

    arm交叉编译器:arm-2009q3.tar.bz2

    内核版本:linux3.0.15,迅为修改后的。

    4.1 驱动框架

      根据前面的说明,编写如下:

    #include <linux/module.h>
    #include <linux/kernel.h>
    
    /*注册设备节点的文件结构体*/
    #include <linux/fs.h>
    /* 驱动的加载,卸载 */
    #include <linux/init.h>
    /*驱动注册的头文件,包含驱动的结构体和注册和卸载的函数*/
    #include <linux/platform_device.h>
    
    #define LED_MAJOR	231
    #define DEVICE_NAME	"leds"
    
    static struct file_operations leds_fops = {
    	.owner = THIS_MODULE,
    	.open = leds_open,
    	.read = leds_read,
    	.write = leds_write,
    };
    
    static struct class *leds_class;
    static struct device *leds_class_devs;
    
    /*
     * 执行insmod命令会调用该函数
     */
    static int __init leds_init(void)
    {
    	int retval;
        
    	/*
    	 * 注册字符类设备
    	 * 参数为 主设备号、设备名字、设备对应的file_operations结构体
    	 * 这样就将设备号与对应结构体绑定,操作设备号的设备文件时,
    	 * 就会调用file_operations的相关成员函数
    	 * 设备号如何写入0,表示由内核自动分配主设备号。
    	 */
    	retval = register_chrdev(LED_MAJOR, DEVICE_NAME, &leds_fops);
    	if (retval < 0) {
    		printk(DEVICE_NAME "can't register major number
    ");
    		return retval;
    	}
    
    	/*
    	 * 自动生成设备节点
    	 */
    	leds_class = class_create(THIS_MODULE, "leds");
    	if (IS_ERR(leds_class))
    		return PTR_ERR(leds_class);
    
    	leds_class_devs = device_create(leds_class, NULL, MKDEV(LED_MAJOR, 0), NULL, "xyz");
    
    	printk(DEVICE_NAME "initialized
    ");
    	return 0;
    }
    
    /*
     * 执行rmmod命令会调用该函数
     */
    static void __exit leds_exit(void)
    {
    	unregister_chrdev(LED_MAJOR, "leds");
    
    	device_unregister(leds_class_devs);
    
    	class_destroy(leds_class);
    }
    
    /*指定驱动程序的初始化函数与卸载函数*/
    module_init(leds_init);
    module_exit(leds_exit);
    
    MODULE_LICENSE("GPL");
    
    

      在完成驱动框架后,根据前面的开发说明,就需要编写驱动具体能够执行的操作。例如 open、read、write等。在应用层中进行系统调用时,操作文件首先就是要 open,因此该操作必不可少。而对应驱动中的 open ,一般用来进行硬件初始化。

    2.2 硬件初始化

      硬件的初始化和STM32一样也分为两种:

    • 一种根据芯片手册,直接控制寄存器。
    • 一种调用提供的库函数。

    2.2.1 根据芯片手册操作寄存器

      在操作寄存器之前,需要了解一个概念,那就是:物理地址与虚拟地址,芯片手册上寄存器的地址是物理地址,而由于我使用的开发板运行在linux系统下,那么就需要转换成虚拟地址。那么如何将物理地址转换成虚拟地址?这就需要借助于 /arch/arm/include/asm/io.h 中的 ioremap()函数与iounmap() 函数。

    /* ioremap iounmap*/
    #include <asm/io.h>
    
    #define ioremap(cookie,size)		__arch_ioremap((cookie), (size), MT_DEVICE)
    
    参数:
    -cookie		//地址
    -size		//大小
    
    

      函数的原型在 /arch/arm/mm/ioremap.c 中。

    (1) 首先在模块加载时,就要映射虚拟地址,则在 leds_init()中添加:

    //注意芯片手册,不同的GPIO基地址差异可能较大
    #define GPIO_BASE_ADDR		0x11000000
    #define GPIO_SIZE			0x0F84
    
    unsigned long pvirtual_addr;	//注意不能申明为static, 否则释放会报错
    
    //led2 GPL2_0
    #define GPL2CON		(*(volatile unsigned long *)(pvirtual_addr + 0x0100))
    #define GPL2DAT		(*(volatile unsigned long *)(pvirtual_addr + 0x0104))
    
    //led3 GPK1_1
    #define GPK1CON		(*(volatile unsigned long *)(pvirtual_addr + 0x0060))
    #define GPK1DAT		(*(volatile unsigned long *)(pvirtual_addr + 0x0064))
    
    在 leds_init()中添加:
    
    //将实际的GPIO地址映射成虚拟地址
    pvirtual_addr = (unsigned long)ioremap(GPIO_BASE_ADDR, GPIO_SIZE);
    
    
    
    static int leds_open(struct inode *inode, struct file *file)
    {
        /* 配置led2、led3引脚为输出 */
    
        GPL2CON &= ~(0xF << (0 * 4));
        GPL2CON |= (0x1 << (0 * 4));
    
        GPK1CON &= ~(0xF << (1 *4));
        GPK1CON |= (0x1 << (1 *4));
    
        /* 默认输出低电平,关闭led */
        GPL2DAT &= ~(1 << 0);
        GPK1DAT &= ~(1 << 1);
    
        //GPL2DAT |= (1 << 0);
        //GPK1DAT |= (1 << 1);
    
        printk(DEVICE_NAME " is open
    ");
    
    	return 0;
    }
    
    static int leds_read(struct file *pfile, char __user *pbuff,
    					size_t count, loff_t *poff)
    {
    	return 0;
    }
    
    static ssize_t leds_write(struct file *pfile, const char __user *pbuf,
    					size_t count, loff_t *poff)
    {
    	int val;
    
    	copy_from_user(&val, pbuf, count);
    
    	switch (val) {
    		case 1:
    			GPL2DAT &= ~(1 << 0);
    			GPK1DAT &= ~(1 << 1);
    
    			//GPL2DAT |= (1 << 0);
    			//GPK1DAT |= (1 << 1);
    			break;
    		case 2:
    			GPL2DAT &= ~(1 << 0);
    			GPK1DAT &= ~(1 << 1);
    
    			GPL2DAT |= (1 << 0);
    			//GPK1DAT |= (1 << 1);
    			break;
    		case 3:
    			GPL2DAT &= ~(1 << 0);
    			GPK1DAT &= ~(1 << 1);
    
    			//GPL2DAT |= (1 << 0);
    			GPK1DAT |= (1 << 1);
    			break;
    		default:
    			break;
    	}
    
    	printk(DEVICE_NAME " write %d
    ", val);
    	return 0;
    }
    
    

    2.2.2 调用提供的库函数

      在该硬件上,调用库函数需要包含以下头文件:

    /*Linux中申请GPIO的头文件*/
    #include <linux/gpio.h>
    /*三星平台的GPIO配置函数头文件*/
    /*三星平台EXYNOS系列平台,GPIO配置参数宏定义头文件*/
    #include <plat/gpio-cfg.h>
    #include <mach/gpio.h>
    /*三星平台4412平台,GPIO宏定义头文件*/
    #include <mach/gpio-exynos4.h>
    
    

      条用库函数实现:

    static int leds_open(struct inode *inode, struct file *file)
    {
        /* 配置led2、led3引脚为输出 */
        s3c_gpio_cfgpin(EXYNOS4_GPL2(0), S3C_GPIO_OUTPUT);
        s3c_gpio_cfgpin(EXYNOS4_GPK1(1), S3C_GPIO_OUTPUT);
    
        gpio_set_value(EXYNOS4_GPL2(0), 0);
        gpio_set_value(EXYNOS4_GPK1(1), 0);
    
        printk(DEVICE_NAME " is open
    ");
    
    	return 0;
    }
    
    static int leds_read(struct file *pfile, char __user *pbuff,
    					size_t count, loff_t *poff)
    {
    	return 0;
    }
    
    static ssize_t leds_write(struct file *pfile, const char __user *pbuf,
    					size_t count, loff_t *poff)
    {
    	int val;
    
    	copy_from_user(&val, pbuf, count);
    
    	switch (val) {
    		case 1:
    			gpio_set_value(EXYNOS4_GPL2(0), 1);
    			gpio_set_value(EXYNOS4_GPK1(1), 1);
    			//printk(DEVICE_NAME " i'm here
    ");
    			break;
    		case 2:
    			gpio_set_value(EXYNOS4_GPL2(0), 1);
    			gpio_set_value(EXYNOS4_GPK1(1), 0);
    			break;
    		case 3:
    			gpio_set_value(EXYNOS4_GPL2(0), 0);
    			gpio_set_value(EXYNOS4_GPK1(1), 1);
    			break;
    		default:
    			break;
    	}
    
    	printk(DEVICE_NAME " write %d
    ", val);
    	return 0;
    }
    
    

    2.3 应用层的测试程序

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <stdio.h>
    
    int main(int argc, char **argv)
    {
    	int fd;
    	int val = 1;
    
    	fd = open("/dev/xyz", O_RDWR);
    	if (fd < 0)
    		printf("can't open is!
    ");
    
    	if (argc != 2) {
    		printf("Usage :
    ");
    		printf("%s <on|off>
    ", argv[0]);
    		return 0;
    	}
    
    	if (strcmp(argv[1], "leds") == 0)
    		val = 1;
    	else if (strcmp(argv[1], "led2") == 0)
    		val = 2;
    	else
    		val = 3;
    
    	write(fd, &val, 4);
    
    	return 0;
    }
    
    

      应用程序通过 fd = open("/dev/xyz", O_RDWR); 打开设备节点文件,对应的字符设备驱动程序中的 leds_open运行,对led的硬件IO口进行初始化。应用程序通过write(fd, &val, 4); 传入参数,对应的字符设备驱动程序中 static ssize_t leds_write(struct file *pfile, const char __user *pbuf, size_t count, loff_t *poff) 运行,val 对应 pbuf, 4 对应count

      那么驱动程序中的 copy_from_user(&val, pbuf, count); 是干什么呢?其实就是根据 pbuf 地址,读取 count 数量的数据,赋值给val,在该程序中相当于 val = *pbuf;。只不过copy_from_user()函数,有更好的扩展性以及稳定性。

      copy_from_user():从用户空间中读取数据到。

      copy_to_user():从内核空间发送数据到用户空间。

    2.4 程序的编译与运行

    2.4.1 字符设备驱动程序的编译与加载

      驱动程序需要借助 Makefile进行编译。内容如下:

    #!/bin/bash
    #通知编译器我们要编译模块的哪些源码
    #这里是编译leds.c这个文件编译成中间文件leds.o,要生成的文件名
    #这里的 -m 选项表示可动态加载的module,如果是静态,也就是和内核一起编译的,改为 -y
    obj-m += leds.o 
    
    #源码目录变量,这里用户需要根据实际情况选择路径
    #作者是将Linux的源码拷贝到目录/home/topeet/android4.0下并解压的
    KDIR := /home/topeet/Android4.0/iTop4412_Kernel_3.0
    
    #当前目录变量
    PWD ?= $(shell pwd)
    
    #make命名默认寻找第一个目标
    #make -C就是指调用执行的路径
    #$(KDIR)Linux源码目录,作者这里指的是/home/topeet/android4.0/iTop4412_Kernel_3.0
    #$(PWD)当前目录变量
    #modules要执行的操作
    all:
    	make -C $(KDIR) M=$(PWD) modules
    		
    #make clean执行的操作是删除后缀为o的文件
    clean:
    	rm -rf *.o
    
    

      将编写的程序和Makefile拷贝到 ubuntu,然后切换到拷贝所在的目录下,执行:make。编译成功会生成 leds.ko文件。将其拷贝到开发板上,通过 insmod、rmmod、lsmod命令进行操作。

      要注意的几点:

      (1) Makefile中的obj-m += leds.o 也决定了生成 .ko 的文件名。

      (2) MakefileKDIR一定不能错,对大小写敏感。

      (3) make 一定是在内核已经编译过的情况下,才能使用。也就是需要将上面/home/topeet/Android4.0/iTop4412_Kernel_3.0路径下源码进行编译,如何编译参考迅为编译内核的视频。

    [root@iTOP-4412]# insmod leds_ioremap.ko 
    [ 3941.561189] ledsinitialized
    [root@iTOP-4412]# lsmod
    leds_ioremap 1742 0 - Live 0xbf010000
    [root@iTOP-4412]# rmmod leds_ioremap
    [root@iTOP-4412]# 
    
    

    2.4.2 测试程序的编译与运行

      将 leds_test.c 拷贝到linux中,然后调用 arm 交叉编译工具进行编译,前提是安装好 arm 交叉编译工具链。

      切换到文件所在目录,运行:arm-none-linux-gnueabi-gcc -o leds_test leds_test.c -static,不同版本的编译工具命令可能不同。

      执行完之后,会生成文件 leds_test

      将其拷贝到开发板上,在文件所放的目录下运行:./leds_test(在已加载驱动模块的条件下)。

    [root@iTOP-4412]# ./leds_test 
    [ 2818.836389] leds is open
    Usage :
    ./leds_test <on|off>
    [root@iTOP-4412]# ./leds_test leds
    [ 2848.438178] leds is open
    [ 2848.439284] leds write 1
    [root@iTOP-4412]# ./leds_test led2
    [ 2851.223358] leds is open
    [ 2851.224464] leds write 2
    [root@iTOP-4412]# ./leds_test led3
    [ 2853.576498] leds is open
    [ 2853.577606] leds write 3
    
    

    参考

    1. 《嵌入式Linux应用开发完全手册》 - 韦东山,19章,20章
    2. 韦东山第一期视频,第十二课
    3. 迅为iTop4412资料
    4. 《Linux设备驱动开发详解:基于最新的Linux 4.0内核》 - 宋宝华
    5. Linux字符设备驱动
    6. Linux字符设备驱动实现
  • 相关阅读:
    1.权限管理系统
    Django实战1-权限管理功能实现-01:搭建开发环境
    Django实战1-权限管理功能实现-02:项目设置
    Django实战1-权限管理功能实现-03:用户认证
    Django实战1-权限管理功能实现-04:系统入口
    Django实战1-权限管理功能实现-05:组织架构的添加
    Django实战1-权限管理功能实现-06:知识扩展-Django表单
    2.项目环境搭建
    mysql 基础
    Spring Security中 SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取当前用户
  • 原文地址:https://www.cnblogs.com/wanjianjun777/p/10483939.html
Copyright © 2011-2022 走看看