首页 > 技术文章 > 字符设备的驱动

wanjianjun777 2019-03-06 16:13 原文

更新记录

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 类型的数据结构中。原型在文件include\linux\fs.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 绑定,从而建立起它们之间的联系。

  通过原型在文件include\linux\fs.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\n");
		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\n", 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\n", 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\n");
		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\n");
	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\n");

	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\n", 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\n");

	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\n");
			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\n", 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!\n");

	if (argc != 2) {
		printf("Usage :\n");
		printf("%s <on|off>\n", 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字符设备驱动实现

推荐阅读