【☀Linux驱动开发笔记☀】新字符设备驱动开发_02

新字符设备驱动

新字符设备驱动原理

首先是动态分配设备号dev_t

旧的驱动设备存在的问题

  1. 设备号是静态分配的,不能动态分配
  2. 设备号是固定的,不能动态改变
  3. 设备号是全局的,不能重复
  4. 设备号是静态的,不能动态添加或删除
  5. 而且十分浪费,比如说现在设led主设备号为200那么次设备号的区间就被浪费了。所以一个led只能有一个主设备号一个次设备号
    因此现在都是动态分配设备号
c 复制代码
int major;//主设备号
int minor;//次设备号
dev_t devid;//设备号

if(major == 0)
{
    alloc_chrdev_region(&devid,0,1,"test");//申请设备号
    //param:1.指向 dev_t 的指针,2.起始设备号这里从 0 开始,3.设备号数量,4.设备名
    major = MAJOR(devid);//获取主设备号
    minor = MINOR(devid);//获取次设备号
}else{
    devid = MKDEV(major,0);//大部分次设备号都是0号
    register_chrdev_region(devid,1,"test");//注册设备号
}

如果想要注销设备号

c 复制代码
unregister_chrdev_region(devid,1);//注销设备号
//param:1.设备号,2.设备号数量

第二步是字符设备结构struct cdev

那么linux中的字符设备结构struct cdev是这样的

c 复制代码
struct cdev{
    struct kobject kobj;//kobject是linux中的对象模型,这里用来表示字符设备
    struct module *owner;//模块指针
    const struct file_operations *ops;//文件操作结构体指针
    struct list_head list;//链表头
    dev_t dev;//设备号
    unsigned int count;//设备号数量
}

这个结构体当中我们要着重关注ops和dev这两个成员

ops是文件操作结构体指针,这里我们要实现自己的文件操作函数

dev是设备号,这里我们要注册自己的设备号,就是我们第一步在做的内容

首先定义一个cdev结构体变量

c 复制代码
struct cdev test_cdev;

第三步是初始化cdev结构体

  1. 先定义一个struct file_operations
c 复制代码
struct file_operations test_fops = {
    .owner = THIS_MODULE,
    .read = test_read,
    .write = test_write,
    .open = test_open,
    .release = test_release,
};
//param:1.模块指针,2.读取函数指针,3.写入函数指针,4.打开函数指针,5.关闭函数指针
  1. dev_init 函数对test_cdev结构体进行初始化重要的成员ops
c 复制代码
test_cdev.owner = THIS_MODULE;//模块指针
cdev_init(&test_cdev,&test_fops);//初始化cdev结构体
//param:1.指向 cdev 结构体的指针,2.指向 file_operations 结构体的指针
  1. dev_add 函数对test_cdev结构体进行初始化重要的成员dev
c 复制代码
test_cdev.dev = devid;//设备号
cdev_add(&test_cdev,devid,1);//添加cdev结构体到系统
//param:1.指向 cdev 结构体的指针,2.设备号,3.设备号数量

如果要卸载驱动就用dev_del函数

c 复制代码
cdev_del(&test_cdev);//删除cdev结构体
//param:1.指向 cdev 结构体的指针

以上三步做完后就是配置好了字符设备的结构体,下一步就是自动创建设备节点

自动创建设备节点

mdev机制

udev是一个用户程序,linux通过udev来实现设备文件的创建与删除,udev可以检测系统中的硬件设备状态,当有新的硬件设备插入时,udev会自动创建对应的设备文件,当硬件设备移除时,udev会自动删除对应的设备文件。比说如当用modprobe命令加载模块的时候就会自动在**/dev目录**下创建对应的设备文件,当用modprobe -r命令卸载模块的时候就会自动删除对应的设备文件。
就是用于实现热拔插的一个用户程序

因此class_create/device_create 让 udev 自动在 /dev 下创建设备节点
为什么要创建这样一个设备文件

因为linux是一个多用户多任务的操作系统,每个用户都有自己的权限,而设备文件是用户访问硬件的接口,所以每个用户都需要有自己的设备文件,而不是所有用户都共享一个设备文件。

  • 不需要为每个用户单独创建设备文件。设备文件通常是系统级的一个节点,所有用户共享同一个 /dev/xxx 。
  • 多用户访问的权限控制依靠设备文件的属主/属组和权限位,或通过 udev 规则统一管理,而不是为每个用户建一份。

class/device 指内核设备模型中的两个对象: struct class 和 struct device 。它们把你的设备以标准化方式呈现在 /sys (sysfs)中,并向用户空间发送热插拔事件(uevent)。

用户空间的 udev 监听这些事件,根据设备信息和规则自动在 /dev 下创建设备节点、设置权限、建立符号链接、做持久命名等。
具体过程

创建类: class_create(THIS_MODULE, "test") → 在 /sys/class/test/ 出现一个类目录。

创建设备: device_create(test_class, NULL, devid, NULL, "test") → 在 /sys/class/test/test/ 出现设备对象,携带 devid (主/次设备号),并触发 uevent 。

第一步创建和删除类

这里的类是什么意思

它是对一类设备的逻辑分组,出现在 /sys/class/<类名>/ 下,便于用户态(如 udev )发现和管理设备。

与字符设备的 cdev 配合使用: cdev 让内核知道你有一个字符设备; class/device 让用户态看到它并创建 /dev/xxx 节点。!!!

自动船舰设备结点的工作是在驱动程序的入口函数中完成的,要在cdev_add函数后添加相关代码

c 复制代码
struct class * test_class = class_create(THIS_MODULE,"test");//创建类
//param:1.模块指针,2.类名

如果要删除类就用class_destroy函数

c 复制代码
class_destroy(test_class);//删除类
//param:1.类指针

第二步创建设备节点

要放在驱动入口函数static int __init led_init(void)中

c 复制代码
struct device *device = device_create(test_class,NULL,devid,NULL,"test");//创建设备节点
//param:1.类指针,2.父设备指针,3.设备号,4.额外数据,5.设备名

如果要删除设备节点就用device_destroy函数

c 复制代码
device_destroy(test_class,devid);//删除设备节点
//param:1.类指针,2.设备号

第三步设置文件私有数据

每一个设备都有一些属性比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,我们最好将这些变量做成一个结构体。在编写驱动open函数的时候将设备结构体作为私有数据添加到设备文件中

c 复制代码
/* 设备结构体 */
 struct test_dev{
    dev_t devid; /* 设备号 */
    struct cdev cdev; /* cdev */
    struct class *class; /* 类 */
    struct device *device; /* 设备 */
    int major; /* 主设备号 */
    int minor; /* 次设备号 */
    struct mutex lock;//可以添加一个互斥锁做并发处理
    char data[100];//内核缓冲区与有效数据长度
    size_t data_len;
 };
 struct test_dev testdev
//open函数
static int test_open(struct inode *inode,struct file *filp)
{
    filp->private_data = &testdev;//将设备结构体添加到设备文件中
    return 0;
}

open 是字符设备被用户态调用 open("/dev/xxx") 时,VFS 进入驱动的入口函数。

它是"每次打开"独有的,不同进程或多次 open 会得到不同的 filp ,这就支持了并发和多实例,可承载不同状态。

在open函数设置好私有数据以后,在write、read、close函数中就可以通过filp->private_data来获取设备结构体

总结一下上面两个大步骤

cdev_add(&cdev, devid, count) 是关键步骤:把 devid 映射到你的 file_operations ,也就是说,把设备号( dev_t ,含主/次设备号)注册到内核的字符设备表,并绑定到你的回调集合 struct file_operations。没有它打开节点也不会进入你的驱动。

device_create(test_class, NULL, devid, NULL, "test") 把设备对象注册到 /sys/class/test/ 并携带设备号,触发节点创建与事件。有 devtmpfs/udev 时,这个设备对象会触发在 /dev 下生成节点(通常名为你传的 "test" 或被 udev 规则重命名),该节点的设备号也会是 249:0 。

这里可以看到在/sys/class/chrdevbase/目录下有一个chrdevbase目录,这个目录就是我们创建的类,在这个目录下有一个chrdevbase设备对象,这个设备对象就是我们创建的设备节点,它的设备号就是249:0(主设备号|次设备号)

然后在/dev目录下也有一个chrdevbase设备节点,这个节点的设备号也就是249:0。所以说这个设备节点就是我们创建的设备文件,用户可以通过这个设备文件来访问我们的设备。这个是udev看到sys里的类和设备对象,然后根据类和设备对象创建出设备节点。

访问路径

  • 用户打开 /dev/test → 读取节点的设备号 → 通过主设备号定位 cdev → 进入 fops->open/read/write/... , filp->private_data 拿到设备上下文。

对共享数据做一个并发的保护-添加一个互斥锁

在驱动的入口函数添加一个互斥锁的初始化

c 复制代码
mutex_init(&testdev.lock);//初始化互斥锁

接下来就是对设备操作函数的编写

先是向设备read函数

c 复制代码
/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
/*
 * 读设备:根据用户请求长度与当前有效数据长度计算实际拷贝字节数
 * 注意:copy_to_user 返回的是"未拷贝字节数",非 0 表示发生错误(-EFAULT)
 * 规范做法是返回实际成功拷贝的字节数,便于用户态依据返回值处理
 */
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    struct chrdevbase_dev *dev = filp->private_data;
    size_t to_copy;
    int not_copied;

    mutex_lock(&dev->lock);//加锁保护共享数据
    to_copy = cnt;
    if (to_copy > dev->data_len)//如果用户请求长度大于有效数据长度
        to_copy = dev->data_len;//实际拷贝字节数为有效数据长度
    not_copied = copy_to_user(buf, dev->data, to_copy);//将数据从内核缓冲区拷贝到用户空间
    mutex_unlock(&dev->lock);//解锁保护共享数据

    if (not_copied)
        return -EFAULT;
    return to_copy;
}

介绍一下copy_to_user这个函数

copy_to_user函数用于将数据从内核缓冲区拷贝到用户空间,它的原型为

c 复制代码
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
//param:1.用户空间地址,2.内核空间地址,3.要拷贝的字节数
//return:返回未拷贝的字节数

向设备写write函数

c 复制代码
/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
/*
 * 写设备:限定拷贝长度避免溢出,并补 '\0' 便于打印
 * 注意:copy_from_user 返回"未拷贝字节数",非 0 表示失败(-EFAULT)
 * 返回成功拷贝的字节数,便于用户态确认写入规模
 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    struct chrdevbase_dev *dev = filp->private_data;
    size_t to_copy;
    int not_copied;

    mutex_lock(&dev->lock);
    to_copy = cnt;
    if (to_copy > sizeof(dev->data) - 1)//如果用户请求长度大于内核缓冲区长度
        to_copy = sizeof(dev->data) - 1;//实际拷贝字节数为内核缓冲区长度减1
    not_copied = copy_from_user(dev->data, buf, to_copy);
    if (!not_copied) {
        dev->data[to_copy] = '\0';
        dev->data_len = to_copy;
    }
    mutex_unlock(&dev->lock);

    if (not_copied)
        return -EFAULT;
    printk("kernel recevdata:%s\r\n", dev->data);
    return to_copy;
}

介绍一下copy_from_user这个函数

copy_from_user函数用于将数据从用户空间拷贝到内核缓冲区,它的原型为

c 复制代码
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
//param:1.内核空间地址,2.用户空间地址,3.要拷贝的字节数
//return:返回未拷贝的字节数

最后是对驱动程序编写对应的应用测试程序

c 复制代码
#include "stdio.h"
#include "unistd.h"//包含标准输入输出函数的头文件
#include "sys/types.h"//包含系统数据类型的头文件
#include "sys/stat.h"//包含文件状态结构体的头文件
#include "fcntl.h"//包含文件操作函数的头文件
#include "stdlib.h"
#include "string.h"
static char usrdata[] = {"usr data!"};

/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数
 * @return 			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;//文件描述符,返回值
	char *filename;//设备文件名
	char readbuf[100], writebuf[100];

	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];//获取设备文件名

	/* 打开驱动文件 */
	fd  = open(filename, O_RDWR);//打开设备文件,读写模式
	if(fd < 0){
		printf("Can't open file %s\r\n", filename);
		return -1;
	}

    if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据:按返回长度打印 */
        retvalue = read(fd, readbuf, 50);//从设备文件读取数据,最多读取50字节
        if(retvalue < 0){
            printf("read file %s failed!\r\n", filename);
        }else{
            if(retvalue > 0 && retvalue < (int)sizeof(readbuf)){
                readbuf[retvalue] = '\0';
            }
            printf("read(%d) data:%s\r\n", retvalue, readbuf);
        }
    }

    if(atoi(argv[2]) == 2){
    /* 向设备驱动写数据:按返回长度确认写入规模 */
        memcpy(writebuf, usrdata, sizeof(usrdata));
        retvalue = write(fd, writebuf, 50);//向设备文件写入数据,最多写入50字节
        if(retvalue < 0){
            printf("write file %s failed!\r\n", filename);
        }else{
            printf("write(%d) done\r\n", retvalue);
        }
    }

	/* 关闭设备 */
	retvalue = close(fd);//关闭设备文件
	if(retvalue < 0){
		printf("Can't close file %s\r\n", filename);
		return -1;
	}

	return 0;
}
相关推荐
大聪明-PLUS2 小时前
在 Linux 上使用实时调度策略运行应用程序
linux·嵌入式·arm·smarc
Awkwardx2 小时前
Linux网络编程—应用层自定义协议与序列化
linux·网络
笨鸟笃行2 小时前
百日挑战——单词篇(第二十四天)
学习
q***9942 小时前
SocketTool、串口调试助手、MQTT中间件基础
单片机·嵌入式硬件·中间件
烤麻辣烫2 小时前
23种设计模式(新手)-7迪米特原则 合成复用原则
java·开发语言·学习·设计模式·intellij-idea
小武~3 小时前
#嵌入式Linux电源管理实战:深入解析CPU调频governor原理与优化
linux
菜鸟-013 小时前
IAP二级启动系统
单片机·嵌入式硬件
red watchma3 小时前
向量表偏移寄存器(Vector Table Offset Register,VTOR)
单片机·嵌入式硬件
开开心心_Every4 小时前
Excel图片提取工具,批量导出无限制
学习·pdf·华为云·.net·excel·harmonyos·1024程序员节