文章目录
开发环境: windows + ubuntu18.04 + 讯为rk3568开发板
注册杂项设备
相较于字符设备,杂项设备有以下两个优点:
- 节省主设备号:杂项设备的主设备号固定为 10,在系统中注册多个 misc 设备驱动时,只需使用子设备号进行区分即可。
- 使用简单:相比如普通的字符设备驱动, misc驱动只需要将基本信息通过结构体传递给相应处理函数即可。
在linxu系统中可使用cat /proc/misc
命令查看系统中的杂项设备。注册杂项设备的步骤:
-
1.填充设备操作集结构体
struct file_operations
; -
2.填充杂项设备结构体
struct miscdevice
; -
3.使用函数
misc_register
注册杂项设备; -
4.使用函数
misc_deregister
卸载杂项设备;
上面三步可使用下面函数直观用表达,即:
c
static struct file_operations xxx_fops{
.owner = THIS_MODULE,
.read = xxx_read,
....
};
struct miscdevice xxx_dev{
.minor = MISC_DYNAMIC_MINOR,
.name = "xxx",
.fops = &xxx_fops
};
static int __init xxx_init(void) //驱动入口函数
{
int ret;
printk(KERN_EMERG "xxx_init\r\n");
ret = misc_register(&xxx_dev);//注册杂项设备
if(ret<0)
{
printk( "misc_register failed\r\n");
return -1;
}
printk( "misc_register ok\r\n");
return 0;
}
static void __exit xxx_exit(void) //驱动出口函数
{
printk(KERN_EMERG "xxx_exit\r\n");
misc_deregister(&xxx_dev); //卸载杂项设备
}
module_init(xxx_init); //注册入口函数
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");
具体实现注册一个杂项设备的示例代码如下:
c
#include <linux/kernel.h>
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h> //注册杂项设备头文件
#include <linux/fs.h> //注册设备节点的文件结构体
#include <linux/uaccess.h>
// 打开杂项设备
int _open(struct inode *inode,struct file*file)
{
printk(KERN_EMERG"hello misc");
return 0;
}
// 关闭杂项设备
int close(struct inode * inode, struct file *file)
{
printk(KERN_EMERG"close");
return 0;
}
// 读取杂项设备中的数据
ssize_t misc_read (struct file *file, char __user *buff, size_t size, loff_t *loff)
{
char kbuff[32] = "kernel";
if(copy_to_user(buff,kbuff,strlen(kbuff)) != 0) // 将内核中的数据给应用
{
printk("copy_to_user error\r\n");
return -1;
}
printk(KERN_EMERG"copy_to_user is successful\r\n");
return size;
}
// 写入数据到杂项设备中
ssize_t misc_write (struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
char kbuff[32] ;
if(copy_from_user(kbuff,buff,size)!= 0) // 从应用那儿获取数据
{
printk("copy_from_user error\r\n");
return -1;
}
printk(KERN_EMERG"copy_from_user data:%s\r\n",kbuff);
return size;
}
// 设备文件描述集
struct file_operations misc_fops ={
.owner = THIS_MODULE,
.open = misc_open,
.release = close,
.read = misc_read,
.write = misc_write
};
struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "hello_misc", // 杂项设备名 注册成功后会在 /dev目录下显示
.fops = &misc_fops
};
// 驱动的入口函数
static int __init misc_init(void)
{
int ret = 0;
ret = misc_register(&misc_dev);
if(ret < 0)
printk(KERN_EMERG"misc register is error\r\n.");
else
printk(KERN_EMERG"misc register is seccussful\r\n.");
return 0;
}
//驱动的出口函数
static void __exit misc_exit(void)
{
misc_deregister(&misc_dev);
printk(KERN_EMERG"baibai\r\n");
}
module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");
编译传送到开发板上后,先试用insmod +驱动名.ko
挂载驱动,其结果为:
上述驱动代码的测试代码如下:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
char filePath[] = "/dev/hello_misc";
// 打开文件
int fd = open(filePath,O_RDWR);
if(fd < 0)
{
printf("opening is failed.\n");
return -1;
}
else
printf("opening is successful.\n");
// 读取
char buff1[32],buff2[32] = "hello this is app";
read(fd,buff1,sizeof(buff1));
printf("buff1 is %s.\n",buff1);
// 写入
write(fd,buff2,sizeof(buff2));
close(fd);
return 0;
}
使用./+程序名
运行测试代码后,得结果如下:
驱动模块传参
总所周知,应用程序传参是通过shell终端传,只要将main函数按照下面格式书写即可完成传参操作
c
int main(int argc ,char *argv[])
{
return 0;
}
相比之下,驱动模块传递参数需要借助其他函数完成传参操作:
1. 传递单个参数给内核
c
module_param(name, type, perm)
参数解释:
-
name:参数名,既是外部参数名,又是内部参数名。
-
type:参数的数据类型,可取int、charp等。
-
perm:访问权限。八进制,如:0777。0表示该参数在文件系统中不可见。
注意:传递字符作为参数时数据类为charp
,而不是char
.
2.传递数组给内核
c
module_param_array(name, type, nump, perm)
- module_param_array(name,type,nump,perm)
- name:数组参数名,既是外部参数,又是内部参数
- type:参数的数据类型
- nump:终端传给数组的实际元素个数(指针变量)
- perm:访问权限,0644。0表示该参数在文件系统中不可见
3.传递字符串给内核
c
module_param_string(name, string, len, perm)
- name:参数名,外部参数名
- string:内部参数名(内部字符数组名)
- len:数组长度
- perm:访问权限,0644。0表示该参数在文件系统中不可见
传递参给内核的作用:
- 1.设置驱动的相关参数,如:设备数量、设备缓冲区大小等等。
- 2.可进行安全校验,放置驱动被他人盗用。
说明 :
参数传递的适用时机为 加载驱动到内核时,命令形式为:insmod +驱动名.ko +参数1名=参数值 参数2名=参数值 ......
。例如下面示例中使用insmod file.ko date=12
传递参数:
c
#include <linux/module.h>
#include <linux/init.h>
// 保存参数
static int date;
static int date1;
static int data[5];
static int count;
static char str[32];
static char *strData;
// 传递单个参数
module_param(date,int,S_IRUGO|S_IWUSR); // 可读可写
module_param(date1,int,S_IRUGO); // 可读可写
module_param(strData,charp,S_IRUGO); // 可读
// 传递多个参数
module_param_array(data,int,&count,S_IRUGO); // 可读
module_param_string(str,str,sizeof(str),S_IRUGO); // 可读
// 驱动的入口函数
static int __init dev_init(void)
{
int i=0;
if(strcmp(str,"myTest")!=0)
{
printk("dev_init error\r\n");
return -1;
}
printk("---------------------------------------\r\n");
printk(KERN_EMERG"dev_init is successful!\r\n");
for(i=0;i<count;++i)
printk("data[%d] = %d \r\n",i,data[i]);
printk("str:%s strData:%s\r\n",str,strData);
printk("date:%d count:%d\r\n",date,count);
data[1] = 111;
date = 10;
date1 = 111;
return 0;
}
//驱动的出口函数
static void __exit dev_exit(void)
{
int i=0;
for( i=0;i<count;++i)
printk("data[%d] = %d \r\n",i,data[i]);
printk("date:%d count:%d\r\n",date,count);
printk(KERN_EMERG"dev_exit is successful\r\n");
}
module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");
注册字符设备
步骤一:驱动初始化,需要申请设备号,初始化并且注册cdev结构体,初始化硬件;
可使用动态申请或静态申请设备号,其中动态申请设备号一般在235-255,静态申请则一般由用户手动输入。
静态申请的函数原型为:
c
int register_chrdev_region(dev_t, unsigned, const char *);
参数含义:
- from: 自定义的 dev_t 类型设备号。
- count: 申请设备的数量。
- name: 申请的设备名称。
函数返回值:申请成功返回 0,申请失败返回负数。
动态申请的函数原型为:
c
int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
参数含义:
- dev : 会将申请完成的设备号保存在 dev 变量中。
- baseminor: 次设备号可申请的最小值。
- count: 申请设备的数量。
- name: 申请的设备名称。
函数返回值:申请成功返回 0,申请失败返回负
Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备号、内核对象、文件操作 file_operations 结构体(设备的打开、读写、关闭等操作接口)等信息:
c
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结
构体.struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
初始化设备描述集cdev结构体的函数原型为:
c
void cdev_init(struct cdev *, const struct file_operations *);
参数含义:
- 参数1:表示是抽象设备结构体;
- 参数2:表示文件操作集;
注册设备到内驱使用下面函数
c
int cdev_add(struct cdev *, dev_t, unsigned);
参数含义:
- 参数1:为要添加的 struct cdev 类型的结构体
- 参数2:为申请的字符设备号
- 参数3:为和该设备关联的设备编号的数量
若函数在内核中添加成功返回 0,添加失败返回负数。
步骤二:构建设备文件操作描述集file_operations
也就是read、write、open、close等函数。
步骤三:生成并且添加设备节点
- 手动添加设备节点 :也就是在加载驱动到内核时添加,即
mknod + 路径/设备名 +设备类型 + 主设备号 + 次设备号
- 自动添加设备节点 :初始化内核时,使用函数自动添加。即先试用函数
class_create
创建一个类,在使用函数device_create
创建并且添加设备节点。
class_create
函数说明:
c
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \ __class_create(owner, name, &__key); \
})
函数作用:
用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进 Linux 内核系统。
参数含义:
- owner:指向函数即将创建的这个 struct class 的模块。一般为 THIS_MODULE。
- name:代表即将创建的 struct class 变量的名字。
返回值:struct class * 类型的类。
device_create
函数说明:
c
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt,...);
函数作用:
用来在 class 类中下创建一个设备属性文件,udev 会自动识别从而进行设备节点的创建。
参数含义:
- cls:指定所要创建的设备所从属的类。
- parent:指定该设备的父设备,如果没有就指定为 NULL。
- devt:指定创建设备的设备号。
- drvdata:被添加到该设备回调的数据,没有则指定为 NULL。
- fmt:添加到系统的设备节点名称。
返回值:struct device * 类型结构体的设备
步骤四:注销字符设备驱动,
- 步骤一:使用函数
unregister_chrdev_region
释放设备号
c
void unregister_chrdev_region(dev_t, unsigned)
该函数只有一个参数,为要删除设备的设备号,并且函数无返回值。
- 步骤二:使用函数
cdev_del
步骤二:删除设备操作集卸载cdev
c
void cdev_del(struct cdev *);
该函数只有一个参数,为要删除的 struct cdev 类型的结构体,并且函数无返回值。
- 步骤三:使用函数
device_destroy
卸载设备
c
void device_destroy(struct class *cls, dev_t devt);
用来删除 cls 类中的devt设备属性文件,udev 会自动识别从而进行设备节点的删除。
- 步骤四:使用函数
class_destroy
删除类class
c
void class_destroy(struct class *cls);
该函数只有一个参数,为要删除的类,并且函数无返回值。
示例代码
c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#define DEVICE_NUMBER 1 // 设备数量
#define DEVICE_SNAME "schrdev" // 静态申请时设备名
#define DEVICE_ANAME "achrdev" // 动态申请时设备名
#define DEVICE_MINOR_NUM 0 // 次设备号起始地址
#define DEVICE_CLASS_NAME "myTestClass" // 类名
#define DEVICE_MYNAME "mytest"
// 打开设备
int chrdev_open(struct inode*inode,struct file*file)
{
printk("chrdev_open is opened\r\n");
return 0;
}
// 保存设备号 其中前12位为主设备号 后20位为次设备号
static dev_t dev_num;
// 定义主设备号 次设备号
static int major_num,minor_num;
// 设备信息描述集
struct cdev cdev;
// 类描述集
struct class *cls;
// 设备描述集
struct device*device;
// 传递单个参数
module_param(major_num,int,S_IRUGO); // 可读
module_param(minor_num,int,S_IRUGO); // 可读
// 文件操作集
struct file_operations chrdev_opr = {
.owner = THIS_MODULE,
.open = chrdev_open
};
// 驱动的入口函数
static int __init dev_init(void)
{
int ret;
/* 步骤一:申请设备号 */
// 有传递主设备号就静态申请
if(major_num)
{
dev_num = MKDEV(major_num, minor_num);//将主设备号 次设备号合并成设备号
//参数分别表示 设备号 设备数量 设备名称
ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME);
if(ret < 0)
{
printk("dev_init error\r\n");
return -1;
}
}
// 否则就动态申请
else
{
// 参数:设备号 次设备号起始地址 设备数量 设备名称
ret = alloc_chrdev_region(&dev_num , DEVICE_MINOR_NUM, DEVICE_NUMBER, DEVICE_ANAME);
if(ret < 0)
{
printk("dev_init error\r\n");
return -1;
}
// 获取主设备号 次设备号
major_num = MAJOR(dev_num);
minor_num = MINOR(dev_num);
}
printk("---------------------------------------\r\n");
printk("dev_num:%d major_num:%d minor_num:%d\r\n",dev_num,major_num,minor_num);
/* 步骤二:初始化设备 */
// 初始化cdev
cdev.owner = THIS_MODULE;
// 初始化设备
cdev_init(&cdev, &chrdev_opr);
/* 步骤三:注册设备到内核 */
//添加(注册)到内核 参数: 设备 设备号 设备数量
cdev_add(&cdev, dev_num, DEVICE_NUMBER);
/* 步骤四:先创建类再自动创建添加设备名称*/
// 创建类 参数1: 类的归属 参数2: 类名
cls = class_create(THIS_MODULE,DEVICE_CLASS_NAME);
// 创建设备 参数1: 归属到类 参数2:设备的父设备 参数3:设备号 参数4:添加到设备的回调数据 参数5:设备名
device = device_create(cls,NULL,dev_num,NULL,DEVICE_MYNAME);
printk("auto add device name\r\n");
return 0;
}
//驱动的出口函数
static void __exit dev_exit(void)
{
/* 步骤一:注销设备号 参数:设备号 设备数量 */
unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER);
/* 步骤二:删除设备操作集 */
cdev_del(&cdev);
/* 步骤三:删除设备*/
device_destroy(cls,dev_num);
/* 步骤四: 删除类*/
class_destroy( cls);
printk(KERN_EMERG"dev_exit is successful\r\n");
}
module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zhouxianjie0716@qq.com");
使用加载驱动到内核insmod file.ko命令,得结果
在测试驱动前需要使用mknod /dev/mytest c 236 0
创建设备文件,命令格式为:mknod + /路径/设备名称 +设备类型+主设备号 + 次设备号
。
然后就可写一个应用程序来测试我们的字符驱动是否成功注册,测试源码如下:
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
char name[] = "/dev/mytest";
int fd = open(name,O_RDONLY); // 仅读
if(fd < 0)
{
printf("open is failed\n");
return -1;
}
close(fd);
return 0;
}
执行测试程序后结果如下:
注意:在运行测试程序时,一定要先创建驱动文件,否则就会报段错误,具体如下:
杂项设备与字符设备的比较:
- 杂项设备的主设备号固定为10,而字符设备的主设备号需要创建。
- 杂项设备的创建相对简单,只需要填充设备操作集结构体、杂项设备结构体再注册即可。而字符设备需要经过 申请设备号、初始化并且注册cdev结构体、初始化硬件、构建设备文件操作描述集、生成并且添加设备节点。
- 杂项设备与字符设备都需要构建文件操作描述集。(应用层调用驱动的核心)