linux新字符设备驱动

linux新字符设备驱动

  • 0 新字符设备注册方法
    • 自动创建设备节点
    • 自定义设备结构体
  • 1 linux系统设备分类
    • 1.1 linux设备驱动之字符设备驱动linux设备驱动之字符设备驱动
    • 1.2 字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系
    • 1.3 用户空间访问该设备的程序
  • 2 字符设备驱动模型
    • 2.1 cdev 结构体解析
    • 2.2 设备号相应操作
  • 实例
    • hello.c
    • 测试程序 test.c
    • makefile

众所周知,字符设备是Linux下最基本,也是最常用到的设备,它是学习linux驱动入门最好的选择。计算机的很多东西都是相通的,掌握了其中一块,其它的就触类旁通了。在写驱动之前,必须先搞清楚字符设备驱动的框架大概是怎样的,一定要弄清楚流程,才开始动手,不要一开始就动手写代码。

0 新字符设备注册方法

使用register_chrdev函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:

①、需要我们事先确定好哪些主设备号没有使用。

②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置LED这个主设备号为200,那么0~1048575(2^20-1)这个区间的次设备号就全部都被LED一个设备分走了。这样太浪费次设备号了!一个LED设备肯定只能有一个主设备号,一个次设备号。

解决这两个问题最好的方法就是要使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。

// 没有指定设备号,申请设备号
// dev : 设备号
// baseminor : 次设备号 , 通常为0
// count : 设备申请数量
// name : 设备名
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)

// 指定设备号,申请设备号
// from : 给定的设备号
// count : 申请设备数量
// name : 设备名称
int register_chrdev_region(dev_t from, unsigned count, const char *name)

// 注销设备
void unregister_chrdev_region(dev_t from, unsigned count)
// 字符设备结构体
struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

// 初始化字符设备
void cdev_init(struct cdev *, const struct file_operations *);

// 添加字符设备
int cdev_add(struct cdev *, dev_t, unsigned);

// 删除字符设备
void cdev_del(struct cdev *);

自动创建设备节点

linux通过udev机制自动创建节点,udev通过访问硬件状态,根据硬件状态申请节点.

自动创建设备节点通过创建类和设备两部组成。

// 创建类
// owner : THIS_MODULE
// name : class name
extern struct class * __must_check __class_create(struct module *owner,
						  const char *name,
						  struct lock_class_key *key);
						  
// 销毁类
extern void class_destroy(struct class *cls);

// 创建设备
struct device *device_create(struct class *cls, struct device *parent,
			     dev_t devt, void *drvdata,
			     const char *fmt, ...);
			     
// 销毁设备
extern void device_destroy(struct class *cls, dev_t devt);

自定义设备结构体

通常设备包含很多属性,如设备号,类,设备等等,采用结构体定义代替变量定义.一般地,在open函数里面将结构作为私有设备添加到设备中。

struct newchrled_dev
{
    dev_t devid;    /*device number */
    struct cdev cdev;   /*cdev */
    struct class *class;    /*class */
    struct device *device; /*device  */
    int major;  /*major device  number*/
    int minor;  /*minor device number */
};

1 linux系统设备分类

原文链接:https://blog.csdn.net/zqixiao_09/article/details/50839042

linux系统将设备分为3类:字符设备、块设备、网络设备。
linux新字符设备驱动_第1张图片

1.1 linux设备驱动之字符设备驱动linux设备驱动之字符设备驱动

字符设备: 是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等。
块设备: 是指可以从设备的任意位置读取一定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。

网络设备: 网络设备比较特殊,不在是对文件进行操作,而是由专门的网络接口来实现。应用程序不能直接访问网络设备驱动程序。在/dev目录下也没有文件来表示网络设备。

每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。

linux新字符设备驱动_第2张图片

1.2 字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系

linux设备驱动之字符设备驱动linux设备驱动之字符设备驱动
linux新字符设备驱动_第3张图片

如图,在Linux内核中使用cdev结构体来描述字符设备,通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性。通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等。

在Linux字符设备驱动中,模块加载函数通过register_chrdev_region( ) 或alloc_chrdev_region( )来静态或者动态获取设备号,通过cdev_init( )建立cdev与file_operations之间的连接,通过cdev_add( )向系统添加一个cdev以完成注册。模块卸载函数通过cdev_del( )来注销cdev,通过unregister_chrdev_region( )来释放设备号。

用户空间访问该设备的程序通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数。

a – 使用cdev结构体来描述字符设备;

b – 通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性;

c – 通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;

在Linux字符设备驱动中:

a – 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;

b – 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;

c – 模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;

1.3 用户空间访问该设备的程序

a – 通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数;

2 字符设备驱动模型

linux新字符设备驱动_第4张图片

2.1 cdev 结构体解析

在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:

<include/linux/cdev.h>
 
struct cdev { 
	struct kobject kobj;                  //内嵌的内核对象.
	struct module *owner;                 //该字符设备所在的内核模块的对象指针.
	const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
	struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表.
	dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.
	unsigned int count;                   //隶属于同一主设备号的次设备号的个数.
};

内核给出的操作struct cdev结构的接口主要有以下几个:
a – void cdev_init(struct cdev *, const struct file_operations *);

其源代码如代码清单如下:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}

该函数主要对struct cdev结构体做初始化, 最重要的就是建立cdev 和 file_operations之间的连接:
(1) 将整个结构体清零;

(2) 初始化list成员使其指向自身;

(3) 初始化kobj成员;

(4) 初始化ops成员;

b --struct cdev *cdev_alloc(void);

该函数主要分配一个struct cdev结构,动态申请一个cdev内存,并做了cdev_init中所做的前面3步初始化工作(第四步初始化工作需要在调用cdev_alloc后,显式的做初始化即: .ops=xxx_ops).

其源代码清单如下:

struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&p->list);
		kobject_init(&p->kobj, &ktype_cdev_dynamic);
	}
	return p;
}

在上面的两个初始化的函数中,我们没有看到关于owner成员、dev成员、count成员的初始化;其实,owner成员的存在体现了驱动程序与内核模块间的亲密关系,struct module是内核对于一个模块的抽象,该成员在字符设备中可以体现该设备隶属于哪个模块,在驱动程序的编写中一般由用户显式的初始化 .owner = THIS_MODULE, 该成员可以防止设备的方法正在被使用时,设备所在模块被卸载。而dev成员和count成员则在cdev_add中才会赋上有效的值。

c – int cdev_add(struct cdev *p, dev_t dev, unsigned count);

该函数向内核注册一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经可以使用了。

当然这里还需提供两个参数:

(1)第一个设备号 dev,

(2)和该设备关联的设备编号的数量。

这两个参数直接赋值给struct cdev 的dev成员和count成员。

d – void cdev_del(struct cdev *p);

该函数向内核注销一个struct cdev结构,即正式通知内核由struct cdev *p代表的字符设备已经不可以使用了。

从上述的接口讨论中,我们发现对于struct cdev的初始化和注册的过程中,我们需要提供几个东西

(1) struct file_operations结构指针;

(2) dev设备号;

(3) count次设备号个数。

但是我们依旧不明白这几个值到底代表着什么,而我们又该如何去构造这些值!

2.2 设备号相应操作

1 – 主设备号和次设备号(二者一起为设备号):

一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

linux内核中,设备号用dev_t来描述,2.6.28中定义如下:

typedef u_long dev_t;

在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。

内核也为我们提供了几个方便操作的宏实现dev_t:

  1. – 从设备号中提取major和minor

MAJOR(dev_t dev);

MINOR(dev_t dev);

  1. – 通过major和minor构建设备号

MKDEV(int major,int minor);
注:这只是构建设备号。并未注册,需要调用 register_chrdev_region 静态申请;

//宏定义:
#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) - 1)
#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))</span>

2、分配设备号(两种方法):

a – 静态申请:

int register_chrdev_region(dev_t from, unsigned count, const char *name);

其源代码清单如下:

int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
	struct char_device_struct *cd;
	dev_t to = from + count;
	dev_t n, next;
 
	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		if (next > to)
			next = to;
		cd = __register_chrdev_region(MAJOR(n), MINOR(n),
			       next - n, name);
		if (IS_ERR(cd))
			goto fail;
	}
	return 0;
fail:
	to = n;
	for (n = from; n < to; n = next) {
		next = MKDEV(MAJOR(n)+1, 0);
		kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
	}
	return PTR_ERR(cd);
}

b – 动态分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

其源代码清单如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
{
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
}

可以看到二者都是调用了__register_chrdev_region 函数,其源代码如下:

static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
			   int minorct, const char *name)
{
	struct char_device_struct *cd, **cp;
	int ret = 0;
	int i;
 
	cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
	if (cd == NULL)
		return ERR_PTR(-ENOMEM);
 
	mutex_lock(&chrdevs_lock);
 
	/* temporary */
	if (major == 0) {
		for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
			if (chrdevs[i] == NULL)
				break;
		}
 
		if (i == 0) {
			ret = -EBUSY;
			goto out;
		}
		major = i;
		ret = major;
	}
 
	cd->major = major;
	cd->baseminor = baseminor;
	cd->minorct = minorct;
	strlcpy(cd->name, name, sizeof(cd->name));
 
	i = major_to_index(major);
 
	for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
		if ((*cp)->major > major ||
		    ((*cp)->major == major &&
		     (((*cp)->baseminor >= baseminor) ||
		      ((*cp)->baseminor + (*cp)->minorct > baseminor))))
			break;
 
	/* Check for overlapping minor ranges.  */
	if (*cp && (*cp)->major == major) {
		int old_min = (*cp)->baseminor;
		int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
		int new_min = baseminor;
		int new_max = baseminor + minorct - 1;
 
		/* New driver overlaps from the left.  */
		if (new_max >= old_min && new_max <= old_max) {
			ret = -EBUSY;
			goto out;
		}
 
		/* New driver overlaps from the right.  */
		if (new_min <= old_max && new_min >= old_min) {
			ret = -EBUSY;
			goto out;
		}
	}
 
	cd->next = *cp;
	*cp = cd;
	mutex_unlock(&chrdevs_lock);
	return cd;
out:
	mutex_unlock(&chrdevs_lock);
	kfree(cd);
	return ERR_PTR(ret);
}

通过这个函数可以看出 register_chrdev_region和 alloc_chrdev_region 的区别,register_chrdev_region直接将Major 注册进入,而 alloc_chrdev_region从Major = 0 开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去;
3、注销设备号:

void unregister_chrdev_region(dev_t from, unsigned count);

4、创建设备文件:

 利用cat /proc/devices查看申请到的设备名,设备号。

1)使用mknod手工创建:mknod filename type major minor

2)自动创建设备节点:

利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

详细解析见:Linux 字符设备驱动开发 (二)—— 自动创建设备节点

实例

练习一下上面的操作:

hello.c

#include 
#include 
#include 
static int major = 250;
static int minor = 0;
static dev_t devno;
static struct cdev cdev;
static int hello_open (struct inode *inode, struct file *filep)
{
	printk("hello_open \n");
	return 0;
}
static struct file_operations hello_ops=
{
	.open = hello_open,			
};
 
static int hello_init(void)
{
	int ret;	
	printk("hello_init");
	devno = MKDEV(major,minor);
	ret = register_chrdev_region(devno, 1, "hello");
	if(ret < 0)
	{
		printk("register_chrdev_region fail \n");
		return ret;
	}
	cdev_init(&cdev,&hello_ops);
	ret = cdev_add(&cdev,devno,1);
	if(ret < 0)
	{
		printk("cdev_add fail \n");
		return ret;
	}	
	return 0;
}
static void hello_exit(void)
{
	cdev_del(&cdev);
	unregister_chrdev_region(devno,1);
	printk("hello_exit \n");
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);

测试程序 test.c

#include 
#include 
#include 
#include 
 
main()
{
	int fd;
 
	fd = open("/dev/hello",O_RDWR);
	if(fd<0)
	{
		perror("open fail \n");
		return ;
	}
 
	close(fd);
}

makefile

ifneq  ($(KERNELRELEASE),)
obj-m:=hello.o
$(info "2nd")
else
KDIR := /lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
all:
	$(info "1st")
	make -C $(KDIR) M=$(PWD) modules
clean:
	rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order
endif

编译成功后,使用 insmod 命令加载:
然后用cat /proc/devices 查看,会发现设备号已经申请成功;

你可能感兴趣的:(嵌入式Linux及驱动开发,linux,运维,服务器)