以下都是我刚开始看驱动视频的个人强行解读,如果有误请指出,共同进步。
- 注册杂项设备
- 生成设备节点
2018-12-07
在本节之前要啰嗦一下,刚开始学的时候,越往后学会越迷糊。所以一定要清楚概念,记不清哪个函数都行,找一找就有了,但一定要理清整个思路,以及意义。比如我现在其实就还比较模糊。
我们之前提到过,主、次设备号这个概念。主设备号有256个,编号是0-255,但是一些常用设备由linux集成进去占用了部分编号,且0和255保留不使用,其他的主设备号,可以由我们去使用。
我们可以用命令cat /proc/devices
查看已有的字符设备的主设备号。
对于本节而言,讲的就是主设备号为10的设备 — 杂项设备
对于一个普通的字符设备来说,我们要注册设备,注册驱动,生成设备节点。
什么是设备节点?上层应用想要操作一个设备,需要打开设备节点来操作,而非直接操作硬件设备(毕竟是系统而非裸机)。这就是设备节点的作用。(大致有这么个概念即可,后面会讲设备节点)
如果每一个设备都按普通的字符设备来注册,有个重要的问题是需要分配主设备号,如果每个设备都分配主设备号无疑是一种资源浪费,而且一系列注册过程中需要调用各种各样的API。
因此,杂项设备出现了,他是一个普通的字符设备的一种封装。
第一,不需要给他分配主设备号,因为杂项设备的主设备号固定是10。
第二,简化步骤,匹配了设备和驱动之后,可以直接生成设备节点。
所以一个杂项设备,我们的步骤应该是,注册platform设备,注册platform驱动,在probe()函数里选择把设备注册成杂项设备(分配次设备号,编写操作函数以供上层调用)。
注册设备和注册驱动之前都做过,所以我们直接在此基础上做模板
#include
#include
// platform相关头文件(设备、驱动的结构体和函数)
#include
// 驱动名(与设备名一致)
#define DRIVER_NAME "mryang_ctl"
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("MrYang");
/* 加载驱动,设备、驱动匹配成功,则调用probe()函数 */
int mryang_probe(struct platform_device *pdv)
{
printk(KERN_EMERG "probe!\n");
return 0;
}
/* 卸载驱动调用remove()函数 */
int mryang_remove(struct platform_device *pdv)
{
printk(KERN_EMERG "remove!\n");
return 0;
}
/* 驱动结构体 */
struct platform_driver mryang_driver = {
.probe = mryang_probe,
.remove = mryang_remove,
.driver = {
.name = DRIVER_NAME,
.owner = THIS_MODULE,
}
};
/* 加载模块 */
static int mryang_init(void)
{
printk(KERN_EMERG "HELLO MrYang\n");
platform_driver_register(&mryang_driver);
return 0;
}
/* 卸载模块 */
static void mryang_exit(void)
{
printk(KERN_EMERG "Bye MrYang\n");
platform_driver_unregister(&mryang_driver);
}
module_init(mryang_init);
module_exit(mryang_exit);
我们需要关注的 就是probe()函数,设备和驱动匹配了,我们下一步就是把设备注册为杂项设备,下面慢慢讲解。
无论注册设备还是驱动,我们都要用API函数以及定义结构体,这次也不例外,我们首先打开杂项设备的头文件看看vim include/linux/miscdevice.h
,搜索结构体miscdevice
,以下就是我们要用到的部分:
#define MISC_DYNAMIC_MINOR 255
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const char *nodename;
mode_t mode;
};
extern int misc_register(struct miscdevice * misc);
extern int misc_deregister(struct miscdevice *misc);
不知道有没有注意到,以往我们代码只看了结构体和注册卸载函数,为什么会把宏定义也放上来呢?因为杂项设备需要分配次设备号,我们可以指定,也可以由linux自动分配,当次设备号被赋值为MISC_DYNAMIC_MINOR时,则表示自动分配次设备号。
首先我们要包含头文件
#include // 注册杂项设备的头文件
我们先定义结构体,然后在probe函数里,把结构体传给注册函数进行注册(卸载同理)
static struct miscdevice mryang_dev= {
// 次设备号,可以指定次设备号是多少,若是想让linux自己去分配,则定义为宏定义
.minor = MISC_DYNAMIC_MINOR,
// 次设备的名字
.name = "mryang_misc_ctl",
.fops = &mryang_ops, // 这个待会儿说
};
我们定义了这个结构体之后,在probe()函数里面注册即可,同理,卸载就在remove()里注册
驱动设备匹配成功,则加载probe(),卸载驱动,则会调用remove()进行移除
那么fops是什么呢?打开结构体看是这么定义的
const struct file_operations *fops;
他是一个文件操作的结构体,对于linux来说,一切皆文件,所以如果我们的应用要操控LED,自然是打开LED的设备节点,写入1开,写入0关(这只是随便举的例子!)。
自然先要包含fs.h
这个头文件,编写file_operations类型的结构体。
结构体很长,就不放上来了,有兴趣最好自己打开看一下struct file_operations
这个结构体。打开之后会发现有很多的函数指针,看名字可以看出来是用于上层应用调用的,比如open(),release()等等,当然这个也有owner,我们赋值为THIS_MODULE即可(以前有讲过),对于这些函数变量,我们也要编写相应的函数,因为下一节会讲编写一个简单的应用去操作设备,现在就是为了给上层调用而写代码。
// 生成设备节点的结构体的头文件
#include
// file_operations 结构体 mryang_ops
static struct file_operations mryang_ops = {
.owner = THIS_MODULE,
// 以下都是函数指针,一会儿还要编写调用函数
// 打开设备节点时调用此函数
.open = mryang_open,
// 关闭设备节点时调用此函数
.release = mryang_release,
// ioctl操作时,调用此函数
.unlocked_ioctl = mryang_unlocked_ioctl,
};
对于这个结构体,我们就简单的写了三个成员函数,打开、关闭、操作。所以接下来我们还要编写这三个函数,当上层应用打开、关闭、操作设备节点时会分别调用这几个函数。
// 3个文件操作函数
static int mryang_open(struct inode *inode, struct file *file){
printk(KERN_EMERG "mryang_open!\n");
return 0;
}
static int mryang_release(struct inode *inode, struct file *file){
printk(KERN_EMERG "mryang_release\n");
return 0;
}
static long mryang_unlocked_ioctl( struct file *files, unsigned int cmd, unsigned long arg)
{
// 打印由上层应用调用ioctl时,参数里cmd的值
printk("cmd is %u\n",cmd);
// 打印由上层应用调用ioctl时,参数里arg的值
printk("arg is %lu\n",arg);
return 0;
}
这样,我们的程序就完成了。
整个程序
#include
#include
#include
#include // 注册杂项设备的头文件
#include // 注册设备节点的结构体的头文件
#define DRIVER_NAME "mryang_ctl"
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("MrYang");
// 打开设备节点时调用此函数
static int mryang_open(struct inode *inode, struct file *file){
printk(KERN_EMERG "mryang_open!\n");
return 0;
}
// 关闭设备节点时调用此函数
static int mryang_release(struct inode *inode, struct file *file){
printk(KERN_EMERG "mryang_release\n");
return 0;
}
// ioctl操作时,调用此函数
static long mryang_unlocked_ioctl( struct file *files, unsigned int cmd, unsigned long arg)
{
// 打印由上层应用调用ioctl时,参数里cmd的值
printk("cmd is %u\n",cmd);
// 打印由上层应用调用ioctl时,参数里arg的值
printk("arg is %lu\n",arg);
return 0;
}
// file_operations 结构体 mryang_ops
static struct file_operations mryang_ops = {
.owner = THIS_MODULE,
// 打开设备节点时调用此函数
.open = mryang_open,
// 关闭设备节点时调用此函数
.release = mryang_release,
// ioctl操作时,调用此函数
.unlocked_ioctl = mryang_unlocked_ioctl,
};
static struct miscdevice mryang_dev= {
// 次设备号,可以指定次设备号是多少,若是想让linux自己去分配,则定义为宏定义
.minor = MISC_DYNAMIC_MINOR,
// 次设备的名字
.name = "mryang_misc_ctl",
.fops = &mryang_ops,
};
/* 加载驱动,设备、驱动匹配成功,则调用probe()函数 */
int mryang_probe(struct platform_device *pdv)
{
printk(KERN_EMERG "probe!\n");
// 注册杂项设备
misc_register(&mryang_dev);
return 0;
}
/* 卸载驱动调用remove()函数 */
int mryang_remove(struct platform_device *pdv)
{
printk(KERN_EMERG "remove!\n");
// 卸载杂项设备
misc_deregister(&mryang_dev);
return 0;
}
/* 驱动结构体 */
struct platform_driver mryang_driver = {
.probe = mryang_probe,
.remove = mryang_remove,
.driver = {
.name = DRIVER_NAME,
.owner = THIS_MODULE,
}
};
/* 加载模块 */
static int mryang_init(void)
{
printk(KERN_EMERG "HELLO MrYang\n");
platform_driver_register(&mryang_driver);
return 0;
}
/* 卸载模块 */
static void mryang_exit(void)
{
printk(KERN_EMERG "Bye MrYang\n");
platform_driver_unregister(&mryang_driver);
}
module_init(mryang_init);
module_exit(mryang_exit);
代码看起来很长,其实分解开来,是很简单的。
这就是整个流程。一步一步来,很清晰。
我们怎么判断是否成功注册了杂项设备呢?
对于注册类函数都有返回值,我们可以声明一个变量,来根据返回值来判断是否注册成功(我的代码懒得写了…比如int ret;
ret = misc_deregister(…),若ret为负数则注册失败,写个if判断一下就行了。)
使用命令 ls /dev
查看所有的设备节点
mryang_misc_ctl 确实出现在了返回的列表里
shellcat /proc/misc
再查看一下杂项设备里有没有我们注册的杂项设备的次设备号。返回 46 mryang_misc_ctl,说明杂项设备的次设备有mryang_misc_ctl,其次设备号为46
对于学习linux,没必要专门去记那些API函数,主要学习的是这些思想,API函数都是可以查到的,记这些干啥,重点关注的是这些思想,这些概念。比如杂项设备和普通字符设备的区别等等…