字符设备驱动程序(二)——手动创建设备节点

手动加载字符设备驱动


手动注册字符驱动流程


  1. 先使用register_chrdev_region()alloc_chrdev_region()来注册一个字符设备的设备号,大家都知道Linux是通过设备号来找到相应的驱动程序的,所以你要注册字符设备的时候,需要一个设备号或系统为你指定一个设备号。
    这两函数的差别就是register_chrdev_region()是注册指定设备号的字符设备,这个如果注册设备号已经存在则有可能会失败,而alloc_chrdev_region()系统分配一个未使用的设备号然后再注册.
  2. 使用cdev_add()函数,把该设备号相应的文件操作file_operation文件操作入口表添加到内核的散列表里面,其中file_operation是需要在驱动里面实现的。然后使用命令mknod,创建与该字符设备(设备号)对应的设备节点,当有用户程序打开这个设备并对其操作时候,系统会调用kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops (file_operation)字段,跳到相应的驱动入口去执行。
  3. 当需要卸载驱动的时候**(手动加载)**,需要做一些清理工作,cdev_del()函数把散列表里面的数据清除,再调用unregister_chrdev_region().

注册字符设备——设备号


  • 系统分配一个未使用的设备号然后再注册----alloc_register_region()
  • 注册指定设备号的字符设备----register_chrdev_region()

字符设备编号
内核中所有已分配的字符设备编号都记录在一个名为 chrdevs 散列表里。该散列表中的每一个元素是一个char_device_struct 结构。

static struct char_device_struct {
	struct char_device_struct *next; // 指向散列冲突链表中的下一个元素的指针
	unsigned int major;           // 主设备号
	unsigned int baseminor;       // 起始次设备号
	int minorct;                 // 设备编号的范围大小
	char name[64];        // 处理该设备编号范围内的设备驱动的名称
	struct file_operations *fops;     
	struct cdev *cdev;        // 指向字符设备驱动程序描述符的指针
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

内核用dev_t类型 (<linux/types.h>) 来保存设备编号,dev_t是一个32位的数,12位表示主设备号,20为表示次设备号。
在实际使用中,是通过<linux/kdev_t.h>中定义的宏来转换格式

(dev_t)–>主设备号、次设备号 MAJOR(dev_t dev) ,MINOR(dev_t dev)
主设备号、次设备号–>(dev_t) MKDEV(int major,int minor)

加载内核模块的时候,会可以cat /proc/devices里面看到你注册的设备和主设备号。

register_chrdev_region()


该函数的作用是,注册一个注定设备号的字符驱动。注册一组设备编号范围(即一个 char_device_struct 结构)
函数原型

int register_chrdev_region(dev_t from, unsigned count, const char *name); //指定设备编号

from: 要分配的设备编号范围的初始值(次设备号常设为0);

count: 连续编号范围.

name: 编号相关联的设备名称. (/proc/devices);

若有重合部分,则register_chrdev_region返回-EBUSY,表示系统正忙,申请的设备号已被占用;若无重合部分,则register_chrdev_region返回0,表示成功


函数 register_chrdev_region()主要执行以下步骤:

  1. 分配一个新的 char_device_struct结构,并用 0 填充
  2. 如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。动态分配主设备号的原则是从散列表的最后一个桶向前寻找,那个桶是空的,主设备号就是相应散列桶的序号。所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。
  3. 根据参数设置char_device_struct 结构中的初始设备号,范围大小及设备驱动名称
  4. 计算出主设备号所对应的散列桶,为新的 char_device_struct结构寻找正确的位置。同时,如果设备编号范围有重复的话,则出错返回。
  5. 将新的 char_device_struct结构插入散列表中,并返回 char_device_struct 结构的地址。

alloc_chrdev_region()


int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);

firstminor: 是请求的最小的次编号;

count: 是请求的连续设备编号的总数;

name: 为设备名,返回值小于0表示分配失败。

返回的设备号存放在 *dev中。

然后通过major=MMOR(dev)获取主设备号。

使用cdev_add函数,把设备号与相应的file_operation指针添加到系统cdev_map 散列表里面去——像内核中添加设备


初始化struct cdev

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

参数
*cdev:cdev结构体。
*fops: file_operations指针。

初始化cdev.owner
cdev.owner = THIS_MODULE;
cdev.ops = &fops;

cdev设置完成,通知内核struct cdev的信息(在执行这步之前必须确定你对struct cdev的以上设置已经完成!)

对于已经知道了主设备号,添加设备就用

int cdev_add(struct cdev *dev,dev_t num,unsigned int count);

第一个参数是设备,第二个参数是设备号,第三个参数是要注册的次设备数目

如果是动态申请的设备号,就用

cdev_add(struct cdev *dev, MKDEV(mem_major, minor), MEMDEV_NR_DEVS); 

第一个参数是设备,第二个参数是设备号,第三个参数是要注册的次设备数目。

完成这一步之后,你已经注册了字符设备,但是你还不能对它进行操作(打开,关闭读,写等),你需要使用mknod命令手动的去生成一个设备号对应的索引节点(设备节点)

cdev结构体


在Linux中使用cdev结构体描述字符设备,cdev结构体定义:

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

kobj 一个嵌入在该结构中的内核对象。它用于该数据结构的一般管理(是一个重要的数据结构,后在后面对其进行详细的介绍)。

owner 指向提供驱动程序的模块。

ops 是一组文件操作,实现了与硬件通信的具体操作。

dev 指定了设备号。

count 表示与该设备关联的从设备的数目。

list 用来实现一个链表,其中包含所有表示该设备的设备特殊文件的inode。

初始化cdev结构体有两种方式

静态:

struct cdev my_cdev;

cdev_init(&my_cdev,&fops);
my_cdev.owner = THIS_MODULE;

动态(就是通过kmalloc去申请cdev结构):

struct cdev *my_cdev = cdev_alloc();

my_cdev->ops= &my_fops;

my_cdev.owner= THIS_MODULE;

注册:

int cdev_add(struct cdev *dev,dev_t num,unsigned int count);

num是设备号,count经常取1

注销:

void cdev_del(struct cdev *dev);

卸载驱动


移除一个字符设备

void cdev_del(struct cdev *p);
unregister_chrdev_region( dev_t dev,MEMDEV_NR_DEVS) //第以个参数是设备号,第二个参数是要注册的次设备数目

手动创建


加载驱动后去查看/proc/devices文件中查看它的主设备号.
通过mknod命令去手动创建,例如:mknod /dev/hello c 250 0/dev/hello为设备节点名字,c代表字符设备,250和0代表它的主次设备号。

字符驱动框架

1. 现将头文件加进来

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

###b 2. 写open,write,read等函数

static int seconds_drv_open(struct inode *inode,struct file *file)
{
	//printk("seconds_drv_open\n");
	/* 配置gpf4,5,6引脚为输出 */
	return 0;
}

static ssize_t seconds_drv_write(struct file *file,const char __user *buf,size_t count,loff_t *ppos)
{
	//printk("seconds_drv_write\n");
	return 0;
}

3. 写file_operations结构,然后将上面的open,write函数填充到相应函数指针处

static struct file_operations seconds_drv_fops={
.owner = THIS_MODULE, 
.open = seconds_drv_open,
.write = seconds_drv_write,
};

4. 在入口函数中注册驱动程序

static int seconds_drv_init(void)
{
	register_chrdev_region(num_dev, number_minor_num, DEVICE_NAME); 

	return 0;
}

5. 在出口函数中卸载驱动

static void seconds_drv_exit(void)
{
	unregister_chrdev_region(MKDEV(major_num, minor_num),number_minor_num);	
}

6. 修饰入口函数和出口函数

module_init(seconds_drv_init);
module_exit(seconds_drv_exit);
MODULE_LICENSE("GPL");

7. 内核传参数,主次设备号

//内核模块传参数
//让内核传进来空闲的主、次设备号
module_param(major_num, int, S_IRUSR);

module_param(minor_num, int, S_IRUSR);

简单字符驱动的编写


char01.c

/*包含初始化宏定义的头文件,代码中的module_init和module_exit在此文件中*/
#include <linux/init.h>
/*包含初始化加载模块的头文件,代码中的MODULE_LICENSE在此头文件中*/
#include <linux/module.h>
/*定义module_param module_param_array的头文件*/
#include <linux/moduleparam.h>
/*定义module_param module_param_array中perm的头文件*/
#include <linux/stat.h>
/*三个字符设备函数*/
#include <linux/fs.h>
/*MKDEV转换设备号数据类型的宏定义*/
#include <linux/kdev_t.h>
/*定义字符设备的结构体*/
#include <linux/cdev.h>
/*分配内存空间函数头文件*/
#include <linux/slab.h>

/*包含函数device_create 结构体class等头文件*/
#include <linux/device.h>


#define DEVICE_NAME "chardevicenode"
#define number_minor_num 2
#define MAJOR_NUM 0 
#define MINOR_NUM 0
#define DEVICE_SIZE 3000


MODULE_LICENSE("Dual BSD/GPL");
/*声明是开源的,没有内核版本限制*/
MODULE_AUTHOR("iTOPEET_dz");
/*声明作者*/

/*主设备号*/
int major_num = MAJOR_NUM;
/*次设备号*/
int minor_num = MINOR_NUM;

/*因为有两个次设备,把cdev设备封装成一个结构体*/
struct reg_dev{
	char * data;
	unsigned long size;
	
	struct cdev dev_cdev;
};
struct reg_dev *mydevice;
//定义一个class
static struct class *myclass; 




static void reg_cdev_init(struct reg_dev * dev, int index)
{
	int err;
	/*偏移量,有不止一个设备*/
	int devno = MKDEV(major_num,minor_num+index);
	
	
	/*设备初始化*/
	cdev_init(&dev->dev_cdev, &my_fops);
	dev->dev_cdev.owner = THIS_MODULE;
	dev->dev_cdev.ops = &my_fops;
	
	
	/*注册设备*/
	err = cdev_add(&dev->dev_cdev,devno,1);
	if(err){
		printk(KERN_EMERG "cdev_add %d is fail! %d\n",index,err);
	}
	else{
		printk(KERN_EMERG "cdev_add %d is success!\n",index);
	}
}

//file_operations
struct file_operations my_fops = {
	.owner = THIS_MODULE,
};


//内核模块传参数
module_param(major_num, int, S_IRUSR);

module_param(minor_num, int, S_IRUSR);

//初始化
static int scdev_init(void)
{
	int ret = 0, i;
	dev_t num_dev;
	
	printk(KERN_EMERG "module_arg1 is %d\n", major_num);
	printk(KERN_EMERG "module_arg2 is %d\n", minor_num);
	
	if(major_num){
		//获得dev_t类型的设备号
		num_dev = MKDEV(major_num, minor_num);
		//静态申请设备号
		ret = register_chrdev_region(num_dev, number_minor_num, DEVICE_NAME);
	}else{
		/*动态注册设备号*/
		ret = alloc_chrdev_region(&num_dev, minor_num, number_minor_num, DEVICE_NAME);
		/*获得主设备号*/
		major_num = MAJOR(num_dev);
		printk(KERN_EMERG "alloc_chrdev_region request is %d\n", major_num);
	}
	if(ret < 0){
		printk(KERN_EMERG "register_chrdev_region request %d is failed\n", major_num);
	} 
	


	/*设备初始化*/
	for(i=0;i<number_minor_num;i++){

		/*注册设备到系统*/
		reg_cdev_init(&mydevice[i], i);
		
}
	
	printk(KERN_EMERG "scdev_init!\n");
	/*打印信息,KERN_EMERG表示紧急信息*/
	return 0;
fail:
	/*注销设备号*/
	unregister_chrdev_region(MKDEV(major_num, minor_num),number_minor_num);
	printk(KERN_EMERG "kmalloc is fail!\n");
	
	return ret;
}

static void scdev_exit(void)
{
	int i;
	printk(KERN_EMERG "scdev WORLD exit!\n");
	
	
	/*除去字符设备*/
	for(i=0;i<number_minor_num;i++){
		cdev_del(&(mydevice[i].dev_cdev))}
	/*释放内存*/
	kfree(mydevice);
	unregister_chrdev_region(MKDEV(major_num, minor_num),number_minor_num);
	
}

module_init(scdev_init);
module_exit(scdev_exit);

测试文件,char01_test.c


#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
/////////////////////////////////////////////////
int main(int argc, char **argv)
{
    int fd;
    fd = open("/dev/chardevicenode", O_RDONLY);
    if (fd < 0)
    {
        printf("open /dev/chardevicenode failed!/n");
        printf("%s/n", strerror(errno));
        return -1;
    }
    printf("open /dev/chardevicenode ok!/n");
    
    close(fd);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_44715649/article/details/88942162