字符类设备

一、申请字符类设备

1.字符设备基本知识

这里开始介绍的是纯粹的字符设备,前面学习的是杂项设备,杂项设备的主设备号已经固定为‘10’。
字符设备分为静态申请和动态申请,静态申请就是主设备号是程序员手动分配了,动态申请是系统给分配,

2.字符设备常用函数

在include/linux/fs.h”中可以找到三个注册字符设备的函数。
这三个分别是函数 register_chrdev_region,函数 alloc_chrdev_region,函数register_chrdev()。
在这里插入图片描述

函数 register_chrdev_region()是提前知道设备的主次设备号,再去申请设备号。
函数 alloc_chrdev_region() 是动态分配主次设备号。
函数 register_chrdev()。是老版本的设备号注册方式,只分配主设备号。从设备号在mknod 的时候指定。这个函数现在虽然仍然可以支持,但是已经不再使用了。

3.cdev结构体

在头文件“include/linux/cdev.h”中,如下图所示。

在这里插入图片描述
如上图所示,cdev 类型是是字符设备描述的结构,其中的设备号必须用“dev_t”类型来描述,高 12 位为主设备号,低 20 位为次设备号

如下图所示,在头文件“include/linux/kdev_t.h”中,有一些专门用来处理 dev_t 数据类型的宏定义。
在这里插入图片描述

MAJOR(dev),就是对 dev 操作,提取高 12 位主设备号;
MINOR(dev) ,就是对 dev 操作,提取低 20 位数次设备号;
MKDEV(ma,mi) ,就是对主设备号和低设备号操作,合并为 dev 类型。

4. 静态申请字符类设备

1.Makefile

#!/bin/bash
#通知编译器我们要编译模块的哪些源码

obj-m += request_cdev_num.o

#源码目录变量,这里用户需要根据实际情况选择路径
#作者是将linux源码拷贝到目录/home/luatao/4412_test/iTop4412_Kernel_3.0
KDIR := /home/luatao/4412_test/iTop4412_Kernel_3.0

#当前目录变量
PWD ?= $(shell pwd)

#make命令默认寻找第一个目标
#make -c就是指调用执行的路径
#$(KDIR)linux的源码目录
#$(PWD)当前目录变量
# modules 要执行的操作
all:
	make -C $(KDIR) M=$(PWD) modules
	
#make clean执行的操作时删除后缀为o的文件
clean:
	rm -rf *.mod.c *.o *.order *.ko *.mod.o *.symvers

2.request_cdev_num.c

#include <linux/init.h>
/* 包含初始化宏定义的头文件,代码中的module_init和module_exit在此文件中*/
#include <linux/module.h>
/* 包含初始化加载模块的头文件,代码中的MODULE_LICENSE在此头文件中 */

/* 定义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>

#define DEVICE_NAME "sscdev" // 设备名
#define DEVICE_MINOR_NUM 2
#define DEV_MAJOR 0  // 主设备号
#define DEV_MINOR 0 // 次设备号


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

int numdev_major = DEV_MAJOR;
int numdev_minor = DEV_MINOR;

/* 调用函数接收参数 */
module_param(numdev_major,int,S_IRUSR); // 主设备号 
module_param(numdev_minor,int,S_IRUSR); // 次设备号

//module_param_array(int_array,int,&int_num,S_IRUSR);

static int scdev_init(void)
{
	int ret = 0;
	dev_t num_dev;
	
	/* 打印单个的参数 */
	printk(KERN_EMERG"numdev_major is %d!\n",numdev_major); // 主设备号
	printk(KERN_EMERG"numdev_minor is %d!\n",numdev_minor); // 次设备号
	
	if(numdev_major)  // 不为0,说明输入有参数
	{
		num_dev = MKDEV(numdev_major,numdev_minor);
		ret = register_chrdev_region(num_dev,DEVICE_MINOR_NUM,DEVICE_NAME);
	}else
	{
		printk(KERN_EMERG"numdev_major %d is failed!\n",numdev_major);
	}
	
	/* 注册失败 */
	if(ret <0)
	{
		printk(KERN_EMERG"register_chrdev_region req %d is failed!\n",numdev_major);
	}

	printk(KERN_EMERG"scdev init!\n");
	/* 打印信息,KERN_EMERG表示紧急信息 */
	return 0;
}


static void scdev_exit(void)
{
	printk(KERN_EMERG"scdev exit!\n");
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
}

/* 初始化函数 */
module_init(scdev_init);
/* 卸载函数 */
module_exit(scdev_exit);

3.编译

在Ubuntu新建一个文件夹

mkdir request_cdev_num

在这里插入图片描述
Make编译
在这里插入图片描述

4.开发板运行

可以使用
cat /proc/devices 查看已经被注册的主设备号,寻找一个没有使用的设备号
在这里插入图片描述
发现9没有使用,那我们就使用9

insmod request_cdev_num.ko numdev_major=9 numdev_minor=0

主设备号是9 次设备号是0
在这里插入图片描述
再次使用cat /proc/devices查看
在这里插入图片描述
可以看到主设备号9已经被驱动所占用。

5.动态申请字符类设备

1.函数参数

extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char );
函数有四个参数
参数
dev,存放返回的设备号;
参数 unsigned,一般为 0;
参数 unsigned,次设备号连续编号范围;
参数 const char *,设备名称;
函数调用成功则返回 0,反之返回-1。

2.Makefile

#!/bin/bash
#通知编译器我们要编译模块的哪些源码

obj-m += request_ascdev_num.o

#源码目录变量,这里用户需要根据实际情况选择路径
#作者是将linux源码拷贝到目录/home/luatao/4412_test/iTop4412_Kernel_3.0
KDIR := /home/luatao/4412_test/iTop4412_Kernel_3.0

#当前目录变量
PWD ?= $(shell pwd)

#make命令默认寻找第一个目标
#make -c就是指调用执行的路径
#$(KDIR)linux的源码目录
#$(PWD)当前目录变量
# modules 要执行的操作
all:
	make -C $(KDIR) M=$(PWD) modules
	
#make clean执行的操作时删除后缀为o的文件
clean:
	rm -rf *.mod.c *.o *.order *.ko *.mod.o *.symvers request_ascdev_num.c

3.request_ascdev_num.c

#include <linux/init.h>
/* 包含初始化宏定义的头文件,代码中的module_init和module_exit在此文件中*/
#include <linux/module.h>
/* 包含初始化加载模块的头文件,代码中的MODULE_LICENSE在此头文件中 */

/* 定义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>

#define DEVICE_NAME "ascdev" // 设备名
#define DEVICE_MINOR_NUM 2
#define DEV_MAJOR 0  // 主设备号
#define DEV_MINOR 0 // 次设备号


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

int numdev_major = DEV_MAJOR;
int numdev_minor = DEV_MINOR;

/* 调用函数接收参数 */
module_param(numdev_major,int,S_IRUSR); // 主设备号 
module_param(numdev_minor,int,S_IRUSR); // 次设备号

//module_param_array(int_array,int,&int_num,S_IRUSR);

static int scdev_init(void)
{
	int ret = 0;
	dev_t num_dev;
	
	/* 打印单个的参数 */
	printk(KERN_EMERG"numdev_major is %d!\n",numdev_major); // 主设备号
	printk(KERN_EMERG"numdev_minor is %d!\n",numdev_minor); // 次设备号
	
	if(numdev_major)  // 不为0,说明输入有参数
	{
		num_dev = MKDEV(numdev_major,numdev_minor);
		ret = register_chrdev_region(num_dev,DEVICE_MINOR_NUM,DEVICE_NAME);
	}else // 不传参数的情况
	{
		/* 动态注册设备号 */
		ret = alloc_chrdev_region(&num_dev,numdev_minor,DEVICE_MINOR_NUM,DEVICE_NAME);
		/* 获得主设备号 */
		numdev_major = MAJOR(num_dev);
		printk(KERN_EMERG"numdev_major req %d!\n",numdev_major);
	}
	
	/* 注册失败 */
	if(ret <0)
	{
		printk(KERN_EMERG"register_chrdev_region req %d is failed!\n",numdev_major);
	}

	printk(KERN_EMERG"scdev init!\n");
	/* 打印信息,KERN_EMERG表示紧急信息 */
	return 0;
}


static void scdev_exit(void)
{
	printk(KERN_EMERG"scdev exit!\n");
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
}

/* 初始化函数 */
module_init(scdev_init);
/* 卸载函数 */
module_exit(scdev_exit);

4.编译

Ubunt新建一个文件夹request_ascdev_num

在这里插入图片描述
Make编译
在这里插入图片描述

5.开发板运行

insmod request_ascdev_num.ko

在这里插入图片描述
这里失败其实是成功了,这是因为代码没有修改,前面的代码已经修改过了。
查看注册的设备号

 cat /proc/devices

在这里插入图片描述

二、注册字符类设备

前面介绍的注册杂项设备,在这一步的处理非常简单,只要在平台文件中添加一个结构体和一个指针调用即可。字符设备和前面的杂项设备唯一的区别就是多了这一步注册设备,难度并不大。

1.分配内存空间

前面介绍的杂项设备并没有分配内存空间这个过程,是因为系统自带的代码已经给杂项设备分配好了。

Linux 中注册字符类设备需要首先申请内存空间,有一个专门分配小内存空间的函数kmalloc,这个函数在头文件“include/linux/slab.h”中,如下图所示,使用命令“vim include/linux/slab.h”打开头文件。
在这里插入图片描述

函数 static inline void *kzalloc(size_t size, gfp_t flags)有两个参数,
参数 size_t size:申请的内存大小(最大 128K),
参数 gfp_t flags:常用参数 GFP_KERNEL,代表优先权,内存不够可以延迟分配。

和申请内存的函数 kzalloc 对应的是 kfree 释放函数,如下图所示
在这里插入图片描述

void kfree(const void *)函数只有一个参数,就是内存的指针,这个指针由申请内存的函数 kzalloc 返回。

2.注册字符类设备的函数

1.cdev_init

注册字符类设备的初始化函数为 cdev_init,这个函数在头文件“include/linux/cdev.h”中,使用命令“vim include/linux/cdev.h”打开这个头文件如下图所示。
在这里插入图片描述

void cdev_init(struct cdev *, const struct file_operations *)”
和结构体“cdev”。

cdev_init 函数有两个参数,
参数 struct cdev *:cdev 字符设备文件结构体
参数 const struct file_operations *:file_operations 结构体,这个结构体已经用过很多次了,就不再介绍了。

2.cdev_add

注册字符设备的函数为 cdev_add,这个函数也是在头文件“include/linux/cdev.h”中,如下图所示。
在这里插入图片描述

注册驱动的函数 int cdev_add(struct cdev *, dev_t, unsigned);有三个参数:
参数 struct cdev *:cdev 字符设备文件结构体
参数 dev_t:设备号 dev,前面已经介绍和使用过了
参数 unsigned:设备范围大小

3.cdev_del

卸载驱动的函数 void cdev_del(struct cdev *)只有一个参数,cdev 字符设备结构体

3.实验操作

1.register_cdev.c

#include <linux/init.h>
/* 包含初始化宏定义的头文件,代码中的module_init和module_exit在此文件中*/
#include <linux/module.h>
/* 包含初始化加载模块的头文件,代码中的MODULE_LICENSE在此头文件中 */

/* 定义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>

#define DEVICE_NAME "ascdev" // 设备名
#define DEVICE_MINOR_NUM 2
#define DEV_MAJOR 0  // 主设备号
#define DEV_MINOR 0 // 次设备号
#define REGDEV_SIZE 3000 // 内存大小


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

int numdev_major = DEV_MAJOR;
int numdev_minor = DEV_MINOR;

/* 调用函数接收参数 */
module_param(numdev_major,int,S_IRUSR); // 主设备号 
module_param(numdev_minor,int,S_IRUSR); // 次设备号

//module_param_array(int_array,int,&int_num,S_IRUSR);


/* 将申请的内存数据控件打包为结构体reg_dev,因为有两个子设备,
所以定义一个结构体数据*my_devices*/
struct reg_dev
{
	char *data;
	unsigned long size;
	
	struct cdev cdev;
};

struct reg_dev *my_devices;

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

/* 自定义函数
   设备注册到驱动
*/
static void reg_init_cdev(struct reg_dev *dev,int index)
{
	int err;
	int devno = MKDEV(numdev_major,numdev_minor + index);
	
	/* 数据初始化 */
	cdev_init(&dev->cdev,&my_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &my_fops;
	
	/* 注册到系统 */
	err = cdev_add(&dev->cdev,devno,1);
	if(err)
	{
		printk(KERN_EMERG"cdev_add %d is failed! %d\n",index,err); 
	}else
	{
		printk(KERN_EMERG"cdev_add %d is success!\n",index); 
	}
}


static int scdev_init(void)
{
	int ret = 0,i;
	dev_t num_dev;
	
	/* 打印单个的参数 */
	printk(KERN_EMERG"numdev_major is %d!\n",numdev_major); // 主设备号
	printk(KERN_EMERG"numdev_minor is %d!\n",numdev_minor); // 次设备号
	
	if(numdev_major)  // 不为0,说明输入有参数
	{
		num_dev = MKDEV(numdev_major,numdev_minor);
		ret = register_chrdev_region(num_dev,DEVICE_MINOR_NUM,DEVICE_NAME);
	}else // 不传参数的情况
	{
		/* 动态注册设备号 */
		ret = alloc_chrdev_region(&num_dev,numdev_minor,DEVICE_MINOR_NUM,DEVICE_NAME);
		/* 获得主设备号 */
		numdev_major = MAJOR(num_dev);
		printk(KERN_EMERG"numdev_major req %d!\n",numdev_major);
	}
	
	/* 注册失败 */
	if(ret <0)
	{
		printk(KERN_EMERG"register_chrdev_region req %d is failed!\n",numdev_major);
	}

	/* 申请内存空间,并将内存空间清零 对设备初始化 */
	my_devices = kmalloc(DEVICE_MINOR_NUM * sizeof(struct reg_dev),GFP_KERNEL);
	if(!my_devices)
	{
		ret = -ENOMEM;
		goto fail;
	}
	/* 清空内存 */
	memset(my_devices,0,DEVICE_MINOR_NUM * sizeof(struct reg_dev));
	
	/* 设备初始化 */
	for(i =0;i < DEVICE_MINOR_NUM;i++)
	{
		my_devices[i].data = kmalloc(REGDEV_SIZE,GFP_KERNEL);
		memset(my_devices[i].data,0,REGDEV_SIZE);
		/* 设备注册到系统 */
		reg_init_cdev(&my_devices[i],i);
	}
	printk(KERN_EMERG"scdev init!\n");
	/* 打印信息,KERN_EMERG表示紧急信息 */
	return 0;
	
fail:
	/* 注销设备号 */
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
	printk(KERN_EMERG"kmalloc is fail!\n");
	return ret;
}


static void scdev_exit(void)
{
	int i;
	printk(KERN_EMERG"scdev exit!\n");
	
	/* 除去字符设备 */
	for(i = 0; i<DEVICE_MINOR_NUM;i++)
	{
		cdev_del(&(my_devices[i].cdev));
	}
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
}

/* 初始化函数 */
module_init(scdev_init);
/* 卸载函数 */
module_exit(scdev_exit);

2.Makefile

#!/bin/bash
#通知编译器我们要编译模块的哪些源码

obj-m += register_cdev.o

#源码目录变量,这里用户需要根据实际情况选择路径
#作者是将linux源码拷贝到目录/home/luatao/4412_test/iTop4412_Kernel_3.0
KDIR := /home/luatao/4412_test/iTop4412_Kernel_3.0

#当前目录变量
PWD ?= $(shell pwd)

#make命令默认寻找第一个目标
#make -c就是指调用执行的路径
#$(KDIR)linux的源码目录
#$(PWD)当前目录变量
# modules 要执行的操作
all:
	make -C $(KDIR) M=$(PWD) modules
	
#make clean执行的操作时删除后缀为o的文件
clean:
	rm -rf *.mod.c *.o *.order *.ko *.mod.o *.symvers

3.编译

新建文件夹 register_cdev
在这里插入图片描述
在这里插入图片描述

4.开发板运行

加载驱动

insmod register_cdev.ko

在这里插入图片描述
设备注册成功

三、生成字符类设备节点

1.创建设备类

在前面介绍的设备中的模型,例如总线 bus、设备 device、驱动 driver 都是有明确的定义。。 bus 代表总线,device 代表实际的设备和接口,driver 代表驱动。

Linux 中的 class 是设备类,它是一个抽象的概念,没有对应的实体。它是提供给用户接口相似的一类设备的集合。常见的有输入子系统 input、usb、串口 tty、块设备 block 等。

以 4412 的串口为例,它有四个串口,不可能为每一个串口都重复申请设备以及设备节点,因为它们有类似的地方,而且很多代码都是重复的地方,所以引入了一个抽象的类,将其打包为 ttySACX,在实际调用串口的时候,只需要修改 X 值,就可以调用不同的串口。

对于本实验中的设备,它有两个子设备,将对应两个设备节点,所以需要用到 class 类这样一个概念。

1. class_create

创建设备类的函数 class_create 在头文件“include/linux/device.h”中,使用命令“vim include/linux/device.h”打开头文件,如下图所示。
在这里插入图片描述

函数 class_create(owner, name)只有两个参数
参数 owner:一般是 THIS_MODULE
参数 name:设备名称
调用函数 class_create 会返回一个 class 结构体变量

2.class 结构体

class 结构体变量在头文件 include/linux/device.h 的 280 行,如下图所示
在这里插入图片描述
在代码中,只需要定义一个 class 变量,然后在创建设备节点的时候作为一个参数赋值使用即可。

3.class_destroy

还有释放设备类 class 的函数 class_destroy,就只有一个参数 class。这个函数也是在头文件“include/linux/device.h”中,如下图所示。
在这里插入图片描述

2.创建字符设备节点

1.device_create

创建设备节点的函数 device_create 在头文件“include/linux/device.h”,如下图所示。
在这里插入图片描述

函数 extern struct device *device_create(struct class *cls, struct device
*parent,dev_t devt, void *drvdata,const char *fmt, …);中的参数比较多。
参数 struct class *cls:设备所属于的类,前面创建类的返回值
参数 struct device *parent:设备的父设备,NULL
参数 dev_t devt:设备号
参数 void *drvdata:设备数据,NULL
参数 const char *fmt:设备名称

2.device_destroy

还有一个摧毁设备节点的函数 extern void device_destroy(struct class
*cls, dev_t devt);只有两个参数,分别是设备类和设备号。

3.实验操作

1.create_cnode.c

#include <linux/init.h>
/* 包含初始化宏定义的头文件,代码中的module_init和module_exit在此文件中*/
#include <linux/module.h>
/* 包含初始化加载模块的头文件,代码中的MODULE_LICENSE在此头文件中 */

/* 定义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 "chardevnode" // 设备名
#define DEVICE_MINOR_NUM 2
#define DEV_MAJOR 0  // 主设备号
#define DEV_MINOR 0 // 次设备号
#define REGDEV_SIZE 3000 // 内存大小


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

int numdev_major = DEV_MAJOR;
int numdev_minor = DEV_MINOR;

/* 调用函数接收参数 */
module_param(numdev_major,int,S_IRUSR); // 主设备号 
module_param(numdev_minor,int,S_IRUSR); // 次设备号

//module_param_array(int_array,int,&int_num,S_IRUSR);

/* 定义一个设备类 */
static struct class *myclass;

/* 将申请的内存数据控件打包为结构体reg_dev,因为有两个子设备,
所以定义一个结构体数据*my_devices*/
struct reg_dev
{
	char *data;
	unsigned long size;
	
	struct cdev cdev;
};

struct reg_dev *my_devices;

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

/* 自定义函数
   设备注册到驱动
*/
static void reg_init_cdev(struct reg_dev *dev,int index)
{
	int err;
	int devno = MKDEV(numdev_major,numdev_minor + index);
	
	/* 数据初始化 */
	cdev_init(&dev->cdev,&my_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &my_fops;
	
	/* 注册到系统 */
	err = cdev_add(&dev->cdev,devno,1);
	if(err)
	{
		printk(KERN_EMERG"cdev_add %d is failed! %d\n",index,err); 
	}else
	{
		printk(KERN_EMERG"cdev_add %d is success!\n",index); 
	}
}


static int scdev_init(void)
{
	int ret = 0,i;
	dev_t num_dev;
	
	/* 打印单个的参数 */
	printk(KERN_EMERG"numdev_major is %d!\n",numdev_major); // 主设备号
	printk(KERN_EMERG"numdev_minor is %d!\n",numdev_minor); // 次设备号
	
	if(numdev_major)  // 不为0,说明输入有参数
	{
		num_dev = MKDEV(numdev_major,numdev_minor);
		ret = register_chrdev_region(num_dev,DEVICE_MINOR_NUM,DEVICE_NAME);
	}else // 不传参数的情况
	{
		/* 动态注册设备号 */
		ret = alloc_chrdev_region(&num_dev,numdev_minor,DEVICE_MINOR_NUM,DEVICE_NAME);
		/* 获得主设备号 */
		numdev_major = MAJOR(num_dev);
		printk(KERN_EMERG"numdev_major req %d!\n",numdev_major);
	}
	
	/* 注册失败 */
	if(ret <0)
	{
		printk(KERN_EMERG"register_chrdev_region req %d is failed!\n",numdev_major);
	}

	/* 申请设备类 */
	myclass = class_create(THIS_MODULE,DEVICE_NAME);
	
	/* 申请内存空间,并将内存空间清零 对设备初始化 */
	my_devices = kmalloc(DEVICE_MINOR_NUM * sizeof(struct reg_dev),GFP_KERNEL);
	if(!my_devices)
	{
		ret = -ENOMEM;
		goto fail;
	}
	/* 清空内存 */
	memset(my_devices,0,DEVICE_MINOR_NUM * sizeof(struct reg_dev));
	
	/* 设备初始化 */
	for(i =0;i < DEVICE_MINOR_NUM;i++)
	{
		my_devices[i].data = kmalloc(REGDEV_SIZE,GFP_KERNEL);
		memset(my_devices[i].data,0,REGDEV_SIZE);
		/* 设备注册到系统 */
		reg_init_cdev(&my_devices[i],i);
		
		/* 创建设备节点 */
		device_create(myclass,NULL,MKDEV(numdev_major,numdev_minor + i),NULL,DEVICE_NAME"%d",i);
	}
	printk(KERN_EMERG"scdev init!\n");
	/* 打印信息,KERN_EMERG表示紧急信息 */
	return 0;
	
fail:
	/* 注销设备号 */
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
	printk(KERN_EMERG"kmalloc is fail!\n");
	return ret;
}


static void scdev_exit(void)
{
	int i;
	printk(KERN_EMERG"scdev exit!\n");
	
	/* 除去字符设备 */
	for(i = 0; i<DEVICE_MINOR_NUM;i++)
	{
		cdev_del(&(my_devices[i].cdev));
		/* 摧毁设备节点*/
		device_destroy(myclass,MKDEV(numdev_major,numdev_minor + i));
	}
	/* 释放设备class*/
	class_destroy(myclass);
	/* 释放内存 */
	kfree(my_devices);
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
}

/* 初始化函数 */
module_init(scdev_init);
/* 卸载函数 */
module_exit(scdev_exit);

2.Makefile

#!/bin/bash
#通知编译器我们要编译模块的哪些源码

obj-m += create_cnode.o

#源码目录变量,这里用户需要根据实际情况选择路径
#作者是将linux源码拷贝到目录/home/luatao/4412_test/iTop4412_Kernel_3.0
KDIR := /home/luatao/4412_test/iTop4412_Kernel_3.0

#当前目录变量
PWD ?= $(shell pwd)

#make命令默认寻找第一个目标
#make -c就是指调用执行的路径
#$(KDIR)linux的源码目录
#$(PWD)当前目录变量
# modules 要执行的操作
all:
	make -C $(KDIR) M=$(PWD) modules
	
#make clean执行的操作时删除后缀为o的文件
clean:
	rm -rf *.mod.c *.o *.order *.ko *.mod.o *.symvers

3.编译

新建一个文件夹
在这里插入图片描述
编译
在这里插入图片描述

4.开发板运行

1.加载驱动

insmod create_cnode.ko

在这里插入图片描述

2.查看class

ls /sys/class/ 

可以查看到生成的 class,如下图所示。
在这里插入图片描述

3.查看设备节点

ls /dev

可以查看到生成的两个设备节点
在这里插入图片描述

4.利用命令创建设备节点

额外知识:
也是可以通过命令来创建设备节点的

mknod /dev/test0 c 249 0

mknod /dev/test1 c 249 1

在这里插入图片描述
这里给设备号 249 申请了两个设备节点,如果设备号 249 有对应的驱动,用命令创建的设备节点和用代码创建的设备节点有一样的效果,都可以给提供给应用程序调用和操作。

四、字符驱动

本实验给大家介绍一个完整的字符驱动,字符驱动的框架结构必须要掌握

1.char_driver.c

#include <linux/init.h>
/* 包含初始化宏定义的头文件,代码中的module_init和module_exit在此文件中*/
#include <linux/module.h>
/* 包含初始化加载模块的头文件,代码中的MODULE_LICENSE在此头文件中 */

/* 定义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 "chardevnode" // 设备名
#define DEVICE_MINOR_NUM 2
#define DEV_MAJOR 0  // 主设备号
#define DEV_MINOR 0 // 次设备号
#define REGDEV_SIZE 3000 // 内存大小


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

int numdev_major = DEV_MAJOR;
int numdev_minor = DEV_MINOR;

/* 调用函数接收参数 */
module_param(numdev_major,int,S_IRUSR); // 主设备号 
module_param(numdev_minor,int,S_IRUSR); // 次设备号

//module_param_array(int_array,int,&int_num,S_IRUSR);

/* 定义一个设备类 */
static struct class *myclass;

/* 将申请的内存数据控件打包为结构体reg_dev,因为有两个子设备,
所以定义一个结构体数据*my_devices*/
struct reg_dev
{
	char *data;
	unsigned long size;
	
	struct cdev cdev;
};

struct reg_dev *my_devices;



static int chardevnode_open(struct inode *inode,struct file *file)
{
	printk(KERN_EMERG"chardevnode_open is success!\n");
	return 0;	
}
static int chardevnode_release(struct inode *inode,struct file *file)
{
	printk(KERN_EMERG"chardevnode_release is success!\n");
	return 0;	
}
/* IO操作 */
static long chardevnode_ioctl(struct file *file,unsigned int cmd,unsigned long arg)
{
	printk(KERN_EMERG"chardevnode_ioctl is success!cmd is %d,arg is %d!\n",cmd,arg);
	return 0;	
}

ssize_t chardevnode_read(struct file *file,char __user *buf,size_t count,loff_t *f_ops)
{
	return 0;	
}
ssize_t chardevnode_write(struct file *file,char __user *buf,size_t count,loff_t *f_ops)
{
	return 0;	
}
loff_t chardevnode_llseek(struct file *file,loff_t offset,int ence)
{
	return 0;	
}
struct file_operations my_fops = {
	.owner = THIS_MODULE,
	.open = chardevnode_open,
	.release = chardevnode_release,
	.unlocked_ioctl = chardevnode_ioctl,
	.read = chardevnode_read,
	.write = chardevnode_write,
	.llseek = chardevnode_llseek,
};


/* 自定义函数
   设备注册到驱动
*/
static void reg_init_cdev(struct reg_dev *dev,int index)
{
	int err;
	int devno = MKDEV(numdev_major,numdev_minor + index);
	
	/* 数据初始化 */
	cdev_init(&dev->cdev,&my_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &my_fops;
	
	/* 注册到系统 */
	err = cdev_add(&dev->cdev,devno,1);
	if(err)
	{
		printk(KERN_EMERG"cdev_add %d is failed! %d\n",index,err); 
	}else
	{
		printk(KERN_EMERG"cdev_add %d is success!\n",index); 
	}
}


static int scdev_init(void)
{
	int ret = 0,i;
	dev_t num_dev;
	
	/* 打印单个的参数 */
	printk(KERN_EMERG"numdev_major is %d!\n",numdev_major); // 主设备号
	printk(KERN_EMERG"numdev_minor is %d!\n",numdev_minor); // 次设备号
	
	if(numdev_major)  // 不为0,说明输入有参数
	{
		num_dev = MKDEV(numdev_major,numdev_minor);
		ret = register_chrdev_region(num_dev,DEVICE_MINOR_NUM,DEVICE_NAME);
	}else // 不传参数的情况
	{
		/* 动态注册设备号 */
		ret = alloc_chrdev_region(&num_dev,numdev_minor,DEVICE_MINOR_NUM,DEVICE_NAME);
		/* 获得主设备号 */
		numdev_major = MAJOR(num_dev);
		printk(KERN_EMERG"numdev_major req %d!\n",numdev_major);
	}
	
	/* 注册失败 */
	if(ret <0)
	{
		printk(KERN_EMERG"register_chrdev_region req %d is failed!\n",numdev_major);
	}

	/* 申请设备类 */
	myclass = class_create(THIS_MODULE,DEVICE_NAME);
	
	/* 申请内存空间,并将内存空间清零 对设备初始化 */
	my_devices = kmalloc(DEVICE_MINOR_NUM * sizeof(struct reg_dev),GFP_KERNEL);
	if(!my_devices)
	{
		ret = -ENOMEM;
		goto fail;
	}
	/* 清空内存 */
	memset(my_devices,0,DEVICE_MINOR_NUM * sizeof(struct reg_dev));
	
	/* 设备初始化 */
	for(i =0;i < DEVICE_MINOR_NUM;i++)
	{
		my_devices[i].data = kmalloc(REGDEV_SIZE,GFP_KERNEL);
		memset(my_devices[i].data,0,REGDEV_SIZE);
		/* 设备注册到系统 */
		reg_init_cdev(&my_devices[i],i);
		
		/* 创建设备节点 */
		device_create(myclass,NULL,MKDEV(numdev_major,numdev_minor + i),NULL,DEVICE_NAME"%d",i);
	}
	printk(KERN_EMERG"scdev init!\n");
	/* 打印信息,KERN_EMERG表示紧急信息 */
	return 0;
	
fail:
	/* 注销设备号 */
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
	printk(KERN_EMERG"kmalloc is fail!\n");
	return ret;
}


static void scdev_exit(void)
{
	int i;
	printk(KERN_EMERG"scdev exit!\n");
	
	/* 除去字符设备 */
	for(i = 0; i<DEVICE_MINOR_NUM;i++)
	{
		cdev_del(&(my_devices[i].cdev));
		/* 摧毁设备节点*/
		device_destroy(myclass,MKDEV(numdev_major,numdev_minor + i));
	}
	/* 释放设备class*/
	class_destroy(myclass);
	/* 释放内存 */
	kfree(my_devices);
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
}

/* 初始化函数 */
module_init(scdev_init);
/* 卸载函数 */
module_exit(scdev_exit);

2.Makefile

#!/bin/bash
#通知编译器我们要编译模块的哪些源码

obj-m += char_driver.o

#源码目录变量,这里用户需要根据实际情况选择路径
#作者是将linux源码拷贝到目录/home/luatao/4412_test/iTop4412_Kernel_3.0
KDIR := /home/luatao/4412_test/iTop4412_Kernel_3.0

#当前目录变量
PWD ?= $(shell pwd)

#make命令默认寻找第一个目标
#make -c就是指调用执行的路径
#$(KDIR)linux的源码目录
#$(PWD)当前目录变量
# modules 要执行的操作
all:
	make -C $(KDIR) M=$(PWD) modules
	
#make clean执行的操作时删除后缀为o的文件
clean:
	rm -rf *.mod.c *.o *.order *.ko *.mod.o *.symvers

3.invoke_char_driver.c

#include <stdio.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

/* 实验现象:*/
main(){
	int fd;
	char *luatao_node0 = "/dev/chardevnode0"; // 这个是自己编写的设备节点 
	char *luatao_node1 = "/dev/chardevnode1"; // 这个是自己编写的设备节点 
	
	/* O_RDWR 只读打开  O_NDELAY非阻塞方式 */
	if((fd = open(luatao_node0,O_RDWR | O_NDELAY)) < 0){
		printf("App open %s failed",luatao_node0);
	}
	else{
		printf("App open %s success",luatao_node0);
	}
	close(fd);
	
	
	/* O_RDWR 只读打开  O_NDELAY非阻塞方式 */
	if((fd = open(luatao_node1,O_RDWR | O_NDELAY)) < 0){
		printf("App open %s failed",luatao_node1);
	}
	else{
		printf("App open %s success",luatao_node1);
	}
	close(fd);
	
}

4.编译

1.编译驱动

新建文件夹

mkdir char_driver

在这里插入图片描述
Make编译
在这里插入图片描述

2.编译应用程序

arm-none-linux-gnueabi-gcc -o invoke_char_driver invoke_char_driver.c -static

在这里插入图片描述

5.开发板运行

1.加载驱动

insmod char_driver.ko 

在这里插入图片描述

2.运行程序

运行应用,调用驱动模块生成的设备节点

./invoke_char_driver

在这里插入图片描述
可以看到链各个设备节点都可以正常打开,说明驱动底层的open函数可以正常使用。

五、字符类GPIOS

本期实验是对前面知识的一个小结,将字符类驱动的框架和 GPIO 操作函数的结合,就可以完成一个字符类 GPIO 驱动。

1.char_driver_leds.h

#ifndef _CHAR_DRIVER_LEDS_H_
#define _CHAR_DRIVER_LEDS_H_

#include <linux/init.h>
/* 包含初始化宏定义的头文件,代码中的module_init和module_exit在此文件中*/
#include <linux/module.h>
/* 包含初始化加载模块的头文件,代码中的MODULE_LICENSE在此头文件中 */

/* 定义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>


#ifndef DEVICE_NAME
#define DEVICE_NAME "chardevnode" // 设备名
#endif

#ifndef DEVICE_MINOR_NUM
#define DEVICE_MINOR_NUM 2
#endif

#ifndef DEV_MAJOR
#define DEV_MAJOR 0  // 主设备号
#endif

#ifndef DEV_MINOR
#define DEV_MINOR 0 // 次设备号
#endif

#ifndef REGDEV_SIZE
#define REGDEV_SIZE 3000 // 内存大小
#endif

/* 将申请的内存数据控件打包为结构体reg_dev*/
struct reg_dev
{
	char *data;
	unsigned long size;
	
	struct cdev cdev;
};

#endif

2.char_driver_leds.c

/* 自定义头文件 */
#include "char_driver_leds.h"
/* GPIO操作的头文件 */
#include <linux/gpio.h>
#include <plat/gpio-cfg.h>
#include <mach/gpio-exynos4.h>



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

/* 将两个gpio定义为数组 */
static int led_gpios[] = {
	EXYNOS4_GPL2(0),EXYNOS4_GPK1(1),
};

#define LED_NUM ARRAY_SIZE(led_gpios)

int numdev_major = DEV_MAJOR;
int numdev_minor = DEV_MINOR;

/* 调用函数接收参数 */
module_param(numdev_major,int,S_IRUSR); // 主设备号 
module_param(numdev_minor,int,S_IRUSR); // 次设备号

//module_param_array(int_array,int,&int_num,S_IRUSR);

/* 定义一个设备类 */
static struct class *myclass;

struct reg_dev *my_devices;

/* 自定义GPIO初始化函数gpio_inti*/
static int gpio_init(void)
{
	int i = 0,ret;
	
	for(i = 0;i<LED_NUM;i++)
	{
		ret = gpio_request(led_gpios[i],"LED");
		if(ret)
		{
			printk("%s:request GPIO %d for LED failed,ret = %d\n",DEVICE_NAME,i,ret);
			return -1;
		}
		else
		{
			s3c_gpio_cfgpin(led_gpios[i],S3C_GPIO_OUTPUT);
			gpio_set_value(led_gpios[i],1);
		}
	}
	return 0;
}

static int chardevnode_open(struct inode *inode,struct file *file)
{
	printk(KERN_EMERG"chardevnode_open is success!\n");
	return 0;	
}
static int chardevnode_release(struct inode *inode,struct file *file)
{
	printk(KERN_EMERG"chardevnode_release is success!\n");
	return 0;	
}
/* IO操作 */
static long chardevnode_ioctl(struct file *file,unsigned int cmd,unsigned long arg)
{
	switch(cmd)
	{
		case 0:
		case 1:
			if(arg > LED_NUM)
			{
				return -EINVAL;
			}
			gpio_set_value(led_gpios[arg],cmd);
			break;
		default:
			return -EINVAL;
	}
	printk(KERN_EMERG"chardevnode_ioctl is success!cmd is %d,arg is %d!\n",cmd,arg);
	return 0;	
}

ssize_t chardevnode_read(struct file *file,char __user *buf,size_t count,loff_t *f_ops)
{
	return 0;	
}
ssize_t chardevnode_write(struct file *file,char __user *buf,size_t count,loff_t *f_ops)
{
	return 0;	
}
loff_t chardevnode_llseek(struct file *file,loff_t offset,int ence)
{
	return 0;	
}
struct file_operations my_fops = {
	.owner = THIS_MODULE,
	.open = chardevnode_open,
	.release = chardevnode_release,
	.unlocked_ioctl = chardevnode_ioctl,
	.read = chardevnode_read,
	.write = chardevnode_write,
	.llseek = chardevnode_llseek,
};


/* 自定义函数
   设备注册到驱动
*/
static void reg_init_cdev(struct reg_dev *dev,int index)
{
	int err;
	int devno = MKDEV(numdev_major,numdev_minor + index);
	
	/* 数据初始化 */
	cdev_init(&dev->cdev,&my_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &my_fops;
	
	/* 注册到系统 */
	err = cdev_add(&dev->cdev,devno,1);
	if(err)
	{
		printk(KERN_EMERG"cdev_add %d is failed! %d\n",index,err); 
	}else
	{
		printk(KERN_EMERG"cdev_add %d is success!\n",index); 
	}
}


static int scdev_init(void)
{
	int ret = 0,i;
	dev_t num_dev;
	
	/* 打印单个的参数 */
	printk(KERN_EMERG"numdev_major is %d!\n",numdev_major); // 主设备号
	printk(KERN_EMERG"numdev_minor is %d!\n",numdev_minor); // 次设备号
	
	if(numdev_major)  // 不为0,说明输入有参数
	{
		num_dev = MKDEV(numdev_major,numdev_minor);
		ret = register_chrdev_region(num_dev,DEVICE_MINOR_NUM,DEVICE_NAME);
	}else // 不传参数的情况
	{
		/* 动态注册设备号 */
		ret = alloc_chrdev_region(&num_dev,numdev_minor,DEVICE_MINOR_NUM,DEVICE_NAME);
		/* 获得主设备号 */
		numdev_major = MAJOR(num_dev);
		printk(KERN_EMERG"numdev_major req %d!\n",numdev_major);
	}
	
	/* 注册失败 */
	if(ret <0)
	{
		printk(KERN_EMERG"register_chrdev_region req %d is failed!\n",numdev_major);
	}

	/* 申请设备类 */
	myclass = class_create(THIS_MODULE,DEVICE_NAME);
	
	/* 申请内存空间,并将内存空间清零 对设备初始化 */
	my_devices = kmalloc(DEVICE_MINOR_NUM * sizeof(struct reg_dev),GFP_KERNEL);
	if(!my_devices)
	{
		ret = -ENOMEM;
		goto fail;
	}
	/* 清空内存 */
	memset(my_devices,0,DEVICE_MINOR_NUM * sizeof(struct reg_dev));
	
	/* 设备初始化 */
	for(i =0;i < DEVICE_MINOR_NUM;i++)
	{
		my_devices[i].data = kmalloc(REGDEV_SIZE,GFP_KERNEL);
		memset(my_devices[i].data,0,REGDEV_SIZE);
		/* 设备注册到系统 */
		reg_init_cdev(&my_devices[i],i);
		
		/* 创建设备节点 */
		device_create(myclass,NULL,MKDEV(numdev_major,numdev_minor + i),NULL,DEVICE_NAME"%d",i);
	}
	
	/* 调用GPIO初始化函数 */
	ret = gpio_init();
	if(ret)
	{
		printk(KERN_EMERG"gpio_init failed!\n");
	}
	
	printk(KERN_EMERG"scdev init!\n");
	/* 打印信息,KERN_EMERG表示紧急信息 */
	return 0;
	
fail:
	/* 注销设备号 */
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
	printk(KERN_EMERG"kmalloc is fail!\n");
	return ret;
}


static void scdev_exit(void)
{
	int i;
	printk(KERN_EMERG"scdev exit!\n");
	
	/* 除去字符设备 */
	for(i = 0; i<DEVICE_MINOR_NUM;i++)
	{
		cdev_del(&(my_devices[i].cdev));
		/* 摧毁设备节点*/
		device_destroy(myclass,MKDEV(numdev_major,numdev_minor + i));
	}
	/* 释放设备class*/
	class_destroy(myclass);
	/* 释放内存 */
	kfree(my_devices);
	
	/* 释放GPIO */
	for(i =0;i<LED_NUM; i++)
	{
		gpio_free(led_gpios[i]);
	}
	unregister_chrdev_region(MKDEV(numdev_major,numdev_minor),DEVICE_MINOR_NUM);
}

/* 初始化函数 */
module_init(scdev_init);
/* 卸载函数 */
module_exit(scdev_exit);

3.invoke_char_gpios.c

#include <stdio.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

/* 实验现象:*/
int main(int argc,char **argv){
	int fd;
	char *led_node = "/dev/chardevnode0"; // 这个是自己编写的设备节点 
	
	/* O_RDWR 只读打开  O_NDELAY非阻塞方式 */
	if((fd = open(led_node,O_RDWR | O_NDELAY)) < 0){
		printf("App open %s failed",led_node);
	}
	else{
		printf("App open %s success",led_node);
		ioctl(fd,atoi(argv[1]),atoi(argv[2]));
		printf("App ioctl %s,cmd id %s! io_arg is %s!\n",led_node,argv[1],argv[2]);
	}
	close(fd);
		
}

4.Makefile

#!/bin/bash
#通知编译器我们要编译模块的哪些源码

obj-m += char_driver_leds.o

#源码目录变量,这里用户需要根据实际情况选择路径
#作者是将linux源码拷贝到目录/home/luatao/4412_test/iTop4412_Kernel_3.0
KDIR := /home/luatao/4412_test/iTop4412_Kernel_3.0

#当前目录变量
PWD ?= $(shell pwd)

#make命令默认寻找第一个目标
#make -c就是指调用执行的路径
#$(KDIR)linux的源码目录
#$(PWD)当前目录变量
# modules 要执行的操作
all:
	make -C $(KDIR) M=$(PWD) modules
	
#make clean执行的操作时删除后缀为o的文件
clean:
	rm -rf *.mod.c *.o *.order *.ko *.mod.o *.symvers

5.编译

1.编译驱动

mkdir char_driver_leds

在这里插入图片描述
编译
在这里插入图片描述

2.编译应用程序

6.开发板运行

1.加载驱动

insmod char_driver_leds.ko 

在这里插入图片描述
这里失败是因为GPIO已经被注册过。

2.运行应用程序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值