一、申请字符类设备
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已经被注册过。