以一个实际例子来学习Linux驱动程序开发之“设备类”的相关知识【利用设备类实现对同一设备类下的多个LED灯实现点亮或关闭】
前言
对于一个设备的驱动程序来说,其实上层用户主要看到的、用到的就是设备文件和设备类,当然用得最多的是设备文件,虽然设备类用得不多,但也是每一个设备注册实例化时必须要用到的东西,本篇博文就以一个简单的例子说明设备类的功能。
设备类的本质
所谓设备类,本质上就是“物以类聚,人以群分”思想的体现,它允许每个设备有一个自己的所属类,说白了就是所属分组,假如某几个设备的所属类是相同的,那么我们就能对这些设备进行一些统一的操作。
下面以一个实际例子看下设备类在Linux嵌入式驱动开发中是如何被定义和使用的。
例子的问题背景和源码
假设我们有 3 个 LED 灯设备(功能相似),它们共享一个驱动程序,每个设备可以独立地开关操作。设备类可以在以下方面帮助实现分组管理:
- 在Linux的 /sys/class/ 目录中,将这些设备归类到一个统一的类目录下。
- 通过类属性实现对所有设备的统一操作,比如一键控制所有 LED 的开关。
我们这里就利用设备类的概念来一键控制所有 LED 的开关。
源码如下:
#include <linux/module.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/cdev.h>#define LED_COUNT 3 // 三个 LED 设备static struct class *led_class;
static struct cdev led_cdev;
static dev_t dev;
static int led_status[LED_COUNT]; // 每个 LED 的状态(0: 关,1: 开)// 模拟控制 LED 的硬件操作
static void led_control(int index, int status)
{printk(KERN_INFO "LED %d is now %s\n", index, status ? "ON" : "OFF");led_status[index] = status;
}// 打开设备的回调函数
static int led_open(struct inode *inode, struct file *file)
{int minor = iminor(inode); // 获取设备次设备号printk(KERN_INFO "LED device %d opened\n", minor);return 0;
}// 写入设备数据的回调函数
static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{int minor = iminor(file_inode(file)); // 获取次设备号int status;// 模拟接收用户的控制命令,'1' 为开,'0' 为关if (copy_from_user(&status, buf, sizeof(int)))return -EFAULT;if (status != 0 && status != 1)return -EINVAL;// 控制对应的 LEDled_control(minor, status);return sizeof(int);
}// 文件操作结构体
static const struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.write = led_write,
};// 统一控制所有 LED 的类属性
static ssize_t led_all_control_store(struct class *cls, struct class_attribute *attr, const char *buf, size_t count)
{int status, i;if (kstrtoint(buf, 10, &status) || (status != 0 && status != 1))return -EINVAL;for (i = 0; i < LED_COUNT; i++)led_control(i, status);return count;
}// 定义类属性
CLASS_ATTR_WO(led_all_control);// 模块初始化函数
static int __init led_init(void)
{int ret, i;// 分配主设备号和次设备号范围ret = alloc_chrdev_region(&dev, 0, LED_COUNT, "led");if (ret < 0) {printk(KERN_ERR "Failed to allocate device numbers\n");return ret;}// 初始化 cdev 并注册cdev_init(&led_cdev, &led_fops);ret = cdev_add(&led_cdev, dev, LED_COUNT);if (ret < 0) {printk(KERN_ERR "Failed to add cdev\n");unregister_chrdev_region(dev, LED_COUNT);return ret;}// 创建设备类led_class = class_create(THIS_MODULE, "led_class");if (IS_ERR(led_class)) {printk(KERN_ERR "Failed to create class\n");cdev_del(&led_cdev);unregister_chrdev_region(dev, LED_COUNT);return PTR_ERR(led_class);}// 添加类属性ret = class_create_file(led_class, &class_attr_led_all_control);if (ret) {printk(KERN_ERR "Failed to create class attribute\n");class_destroy(led_class);cdev_del(&led_cdev);unregister_chrdev_region(dev, LED_COUNT);return ret;}// 为每个 LED 创建设备文件for (i = 0; i < LED_COUNT; i++) {device_create(led_class, NULL, MKDEV(MAJOR(dev), MINOR(dev) + i), NULL, "led%d", i);}printk(KERN_INFO "LED driver loaded\n");return 0;
}// 模块退出函数
static void __exit led_exit(void)
{int i;// 删除每个 LED 的设备文件for (i = 0; i < LED_COUNT; i++) {device_destroy(led_class, MKDEV(MAJOR(dev), MINOR(dev) + i));}// 删除类属性class_remove_file(led_class, &class_attr_led_all_control);// 销毁设备类class_destroy(led_class);// 删除 cdevcdev_del(&led_cdev);// 注销设备号unregister_chrdev_region(dev, LED_COUNT);printk(KERN_INFO "LED driver unloaded\n");
}module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL");
以下我们开始对源码进行分析,源码分析完,那么“设备类”的相关知识就搞懂了。
驱动模块加载代码module_init(led_init);
module_init(led_init);
这行代码,将led_init 函数注册为模块的初始化函数。如果你编译出的模块文件名字为led_driver.ko
,那么当你运行 insmod led_driver.ko 时,内核会自动调用函数 led_init。
驱动模块加载代码module_exit(led_exit);
如果理解了驱动模块加载代码module_init(led_init);
,那么这句代码就没什么好理解了。
模块许可证申明代码MODULE_LICENSE("GPL");
关于这句代码的详细介绍见我的另一篇博文
https://blog.csdn.net/wenhao_ir/article/details/144902881
模块初始化函数led_init()
分析
源码
// 模块初始化函数
static int __init led_init(void)
{int ret, i;// 分配主设备号和次设备号范围ret = alloc_chrdev_region(&dev, 0, LED_COUNT, "led");if (ret < 0) {printk(KERN_ERR "Failed to allocate device numbers\n");return ret;}// 初始化 cdev 并注册cdev_init(&led_cdev, &led_fops);ret = cdev_add(&led_cdev, dev, LED_COUNT);if (ret < 0) {printk(KERN_ERR "Failed to add cdev\n");unregister_chrdev_region(dev, LED_COUNT);return ret;}// 创建设备类led_class = class_create(THIS_MODULE, "led_class");if (IS_ERR(led_class)) {printk(KERN_ERR "Failed to create class\n");cdev_del(&led_cdev);unregister_chrdev_region(dev, LED_COUNT);return PTR_ERR(led_class);}// 添加类属性ret = class_create_file(led_class, &class_attr_led_all_control);if (ret) {printk(KERN_ERR "Failed to create class attribute\n");class_destroy(led_class);cdev_del(&led_cdev);unregister_chrdev_region(dev, LED_COUNT);return ret;}// 为每个 LED 创建设备文件for (i = 0; i < LED_COUNT; i++) {device_create(led_class, NULL, MKDEV(MAJOR(dev), MINOR(dev) + i), NULL, "led%d", i);}printk(KERN_INFO "LED driver loaded\n");return 0;
}
函数声明static int __init led_init(void)
这句代码关键是要理解__init
是怎么回事儿?
详情见 https://blog.csdn.net/wenhao_ir/article/details/144903805
分配主设备号和次设备号范围的代码
// 分配主设备号和次设备号范围ret = alloc_chrdev_region(&dev, 0, LED_COUNT, "led");if (ret < 0) {printk(KERN_ERR "Failed to allocate device numbers\n");return ret;}
这段代码主要是理解函数alloc_chrdev_region()
,关于这个函数的理解见博文 https://blog.csdn.net/wenhao_ir/article/details/144888989 【搜索关键字“第一步是调用函数alloc_chrdev_region”】
初始化 cdev 结构体→将cdev 结构体和file_operations结构体绑定的代码,→写入设备号信息到cdev 结构体
// 初始化 cdev 并注册cdev_init(&led_cdev, &led_fops);ret = cdev_add(&led_cdev, dev, LED_COUNT);if (ret < 0) {printk(KERN_ERR "Failed to add cdev\n");unregister_chrdev_region(dev, LED_COUNT);return ret;}
这段代码主要是理解函数cdev_init
和函数cdev_add
,关于这两个函数的理解见博文 https://blog.csdn.net/wenhao_ir/article/details/144888989 【搜索关键字“第二步是调用函数cdev_init()”和关键字“第三步是调用函数cdev_add”】
在这里我们需要去看下file_operations结构体的实例led_fops
的实现,它里面的成员函数其实才是对设备的具体操作,才是驱动程序的核心。
file_operations结构体的实例led_fops
// 文件操作结构体
static const struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.write = led_write,
};
这个结构体成员中对成员open和write的赋值是我们自己定义的两个函数led_open和led_write,很好理解。但是对成员owner赋值为THIS_MODULE就不理解了,所以专门写了篇博文来理解这个问题,详情见 https://blog.csdn.net/wenhao_ir/article/details/144906774
★★创建设备类的代码★★
这里是我们这篇博文重点关注的问题。
// 创建设备类led_class = class_create(THIS_MODULE, "led_class");if (IS_ERR(led_class)) {printk(KERN_ERR "Failed to create class\n");cdev_del(&led_cdev);unregister_chrdev_region(dev, LED_COUNT);return PTR_ERR(led_class);}
这段创建设备类的代码其实在理解第一个参数宏THIS_MODULE
的定义、作用、原理后(详情见 https://blog.csdn.net/wenhao_ir/article/details/144906774),就很好理解了,第二个参数led_class
就是设备类的名字,注意,这里的类不是面向对象编程中的类的概念,而是分组、分类的意思。
当代码:
led_class = class_create(THIS_MODULE, "led_class");
运行完成后,系统目录/sys/class/
下会新加一个名叫led_class
的目录,即存在了下面这个目录路径:
/sys/class/led_class/
★★★添加设备的类属性的代码(class_create_file函数及与设备类属性有关的重要参数class_attr_led_all_control的分析)★★★
// 添加类属性ret = class_create_file(led_class, &class_attr_led_all_control);if (ret) {printk(KERN_ERR "Failed to create class attribute\n");class_destroy(led_class);cdev_del(&led_cdev);unregister_chrdev_region(dev, LED_COUNT);return ret;}
这里是设备类这个知识点比较难理解的地方。
显然,重点是理解函数class_create_file()
。
函数 class_create_file()
是一个用于在指定的设备类(struct class)中创建属性文件的函数。它为该设备类在 /sys/class/ 下的目录中添加一个用户可访问的文件,即为这个类添加一个属性,这个属性中有相应的操作函数,比如读操作函数、写操作函数。
它的函数原型如下:
int class_create_file(struct class *cls, const struct class_attribute *attr);
cls
: 指向设备类(struct class
)的指针,通常由class_create
创建。attr
: 指向struct class_attribute
的指针,用于定义设备类属性文件的属性和操作。
返回值:
- 返回 0 表示成功。
- 返回负数表示失败,例如内存分配失败或文件创建失败。
第一个参数cls
已经在上面通过下面的代码得到了:
led_class = class_create(THIS_MODULE, "led_class");
第二个参数const struct class_attribute *attrr = &class_attr_led_all_control
的分析是难点,但还是要硬着头皮上…
前方高能,接下来是的内容有如下这些:
前方高能,接下来是的内容有如下这些:
前方高能,接下来是的内容有如下这些:
- 宏
CLASS_ATTR_WO(led_all_control);
的初步展开 - 结构体
struct class_attribute
的定义 - 结构体
struct attribute
的定义 - 宏
__ATTR_WO(led_all_control)
的展开 - 宏
__ATTR(led_all_control, 0200, NULL, led_all_control_store)
的展开 - 宏
CLASS_ATTR_WO(led_all_control);
的彻底展开
宏CLASS_ATTR_WO(led_all_control);
的初步展开
回到问题本身,要理解函数 class_create_file
的关键是要理解第二个参数const struct class_attribute *attr
,首先我们要看第二个参数*attr
被赋值为 &class_attr_led_all_control
,那我们就需要去看下变量class_attr_led_all_control
是在代码中的哪里被定义的?
变量class_attr_led_all_control
实际上在整个代码中你找不到它的显式定义的,实际上是它是在前面的第68行的代码中被定义的:
CLASS_ATTR_WO(led_all_control);
这是一个宏定义,CLASS_ATTR_WO
这个宏的定义如下:
#define CLASS_ATTR_WO(_name) struct class_attribute class_attr_##_name = __ATTR_WO(_name)
关于上面这个宏定义中标记分隔符##
的详解见我的另一篇博文 https://blog.csdn.net/wenhao_ir/article/details/144908107
明白标记分隔符##
的使用后,可将宏初步展开为:
struct class_attribute class_attr_led_all_control = __ATTR_WO(led_all_control)
你看这里面不是有结构体class_attribute的实例class_attr_led_all_control`了吗?然后等号右边又是一个宏定义:
__ATTR_WO(led_all_control)
这个宏的定义如下:
#define __ATTR_WO(_name) _ATTR(_name, 0200, NULL, _name##_store)
所以进一步展开后为:
struct class_attribute class_attr_led_all_control = __ATTR(led_all_control, 0200, NULL, led_all_control_store);
而__ATTR
又是一个宏定义,在解读它之前我们先要搞清楚结构体class_attribute
的定义:
struct class_attribute {struct attribute attr; // 包含类属性的基本信息ssize_t (*show)(struct class *class, struct class_attribute *attr, char *buf);ssize_t (*store)(struct class *class, struct class_attribute *attr, const char *buf, size_t count);
};
结构体class_attribute的第一个成员是一个结构体:struct attribute attr;
,它的定义如下:
struct attribute {const char *name; // 类属性的名称umode_t mode; // 类属性的文件权限
};
结构体class_attribute的第二个成员show是一个函数指针,对应的函数实际上是这个设备类属性的读取函数,当这个设备类属性要进行读操作时,就调用这个函数,这里可以是用户定义的读取函数,也可以是 NULL
(表示不可读)。
结构体class_attribute的第三个成员store是一个函数指针,对应的函数实际上是这个设备类属性的写入函数,当这个设备类属性要进行写操作时,就调用这个函数,这里可以是用户定义的写入函数,也可以是 NULL
(表示不可写)。
有了上面两个结构体的定义之后我们再来看__ATTR
宏,它的定义如下:
以下是 __ATTR
的典型定义(可能会因内核版本略有不同):
#define __ATTR(_name, _mode, _
show, _store) { \.attr = { .name = _name, .mode = _mode }, \.show = _show, \.store = _store, \
}
你看它的内容:
{ \.attr = { .name = _name, .mode = _mode }, \.show = _show, \.store = _store, \
}
不正是结构体class_attribute的主体部分吗?所以它相当于初始化了一个名叫class_attr_led_all_control
的结构体。其中的
所以我们把宏CLASS_ATTR_WO(led_all_control);
彻底展开后的内容如下:
struct class_attribute class_attr_led_all_control = {.attr = {.name = "led_all_control",.mode = 0200,},.show = NULL,.store = led_all_control_store,
};
到这里,我们就算真正的把代码led_class = class_create(THIS_MODULE, "led_class");
中的第二个参数搞清楚了,它的定义和初始化如下:
struct class_attribute class_attr_led_all_control = {.attr = {.name = "led_all_control",.mode = 0200,},.show = NULL,.store = led_all_control_store,
};
我们再来说说各成员的意义:
- name表示这个类属性的名称,在这里类属性的名称为
led_all_control
; - mode表示这个类属性的读写权限,这里的
0200
表示权限为只写; - show表示这个类属性的读操作函数;
- store表示这个类属性的写操作函数。
当下面的代码:
ret = class_create_file(led_class, &class_attr_led_all_control);
运行完成后,/sys/class/led_class/ 目录中增加了下面这个文件:
led_all_control
为每个LED设备创建设备文件
// 为每个 LED 创建设备文件for (i = 0; i < LED_COUNT; i++) {device_create(led_class, NULL, MKDEV(MAJOR(dev), MINOR(dev) + i), NULL, "led%d", i);}
这个就没啥好讲的了,在之前的博文 https://blog.csdn.net/wenhao_ir/article/details/144888989 已经把函数device_create()的使用、主设备号、次设备号讲清楚了。
不过在这里,对第一个参数,即设备类struct class *cls = led_class,
有了认识,之前完全不知道设备类是怎么回事儿。
上面这段代码运行完后:
在系统的/dev/
目录下有了下面这些文件:
/dev/led0
/dev/led1
/dev/led2
在系统的/sys/class/led_class/
目录的有下面这4个文件:
/sys/class/led_class/led_all_control
/sys/class/led_class/led0
/sys/class/led_class/led1
/sys/class/led_class/led2
模块退出函数led_init()
分析
// 模块退出函数
static void __exit led_exit(void)
{int i;// 删除每个 LED 的设备文件for (i = 0; i < LED_COUNT; i++) {device_destroy(led_class, MKDEV(MAJOR(dev), MINOR(dev) + i));}// 删除类属性class_remove_file(led_class, &class_attr_led_all_control);// 销毁设备类class_destroy(led_class);// 删除 cdevcdev_del(&led_cdev);// 注销设备号unregister_chrdev_region(dev, LED_COUNT);printk(KERN_INFO "LED driver unloaded\n");
}
关于这个函数声明行中关键字__exit
的理解,可在对关键字“__init”的理解基础上理解(详情见 https://blog.csdn.net/wenhao_ir/article/details/144888989 其实在这篇博文中也讲了对__exit
的理解和作用)
关于退出函数,关键是要注意资源的释放顺序,顺序就是谁最后被创建,谁最后被销毁。
底层实现函数(具体操作硬件的函数)
下面这些函数都是具体操作硬件的函数
led_control
led_open
led_write
led_all_control_store
这里就不去理它们内部的逻辑了,只说下作用:
led_open就是打开设备的函数;
led_write就是单个LED设备的设备文件的写函数;
led_control是真正控制LED设备的函数,led_write会调用它;
led_all_control_store是设备类的写函数,它实现对这3个LED设备进行统一点亮或关闭。
驱动模块加载之后,怎么样利用设备类将3个LED设备统一关闭或点亮?
驱动模块加载之后,下面这个示例代码即可实现将3个LED设备统一关闭或点亮:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>#define LED_CLASS_ATTR_PATH "/sys/class/led_class/led_all_control"void control_leds(int status) {int fd;char buffer[16];// 打开类属性文件fd = open(LED_CLASS_ATTR_PATH, O_WRONLY);if (fd < 0) {perror("Failed to open LED class attribute file");exit(EXIT_FAILURE);}// 写入状态(0 或 1)snprintf(buffer, sizeof(buffer), "%d", status);if (write(fd, buffer, sizeof(buffer)) < 0) {perror("Failed to write to LED class attribute file");close(fd);exit(EXIT_FAILURE);}printf("Successfully set all LEDs to %s\n", status ? "ON" : "OFF");// 关闭文件close(fd);
}int main() {// 点亮所有 LEDcontrol_leds(1);// 延时 2 秒sleep(2);// 关闭所有 LEDcontrol_leds(0);return 0;
}
程序说明
- 路径定义:
LED_CLASS_ATTR_PATH
定义了类属性文件路径,需与驱动生成的路径一致。 - 文件操作: 使用
open
打开类属性文件,使用write
将状态值写入文件。 - LED 控制: 程序调用
control_leds
函数,参数为1
表示点亮,0
表示关闭。
输出结果
运行程序后,可以观察到:
- 所有 LED 点亮,延时 2 秒后关闭。
- 控制状态在
/sys/class/led_class/led_all_control
中生效,同时会打印相应的日志信息。
注意事项
- 确保
/sys/class/led_class/led_all_control
文件存在。 - 如果遇到权限问题,可以手动修改类属性文件权限:
chmod 666 /sys/class/led_class/led_all_control
- 如果需要更复杂的控制逻辑,可以扩展
control_leds
函数以支持读取状态或处理错误。
从上面这个代码中我们可以看出,驱动模块加载后,咱们通过调用系统函数open()和write()对类属性文件就可以实现对具体设备的操作。
这里要特别注意:代码write(fd, buffer, sizeof(buffer))调用的底层写函数应该是代码中的函数led_all_control_store,而不是函数led_write,详细说明如下:
在示例代码中:
write(fd, buffer, sizeof(buffer));
这个调用的确对应驱动中定义的类属性写函数 led_all_control_store
,而不是 led_write
。
原因分析
-
类属性文件:
/sys/class/led_class/led_all_control
是通过class_create_file
创建的类属性文件。- 类属性文件操作(如读/写)由相应的
store
和show
函数处理,在例子中是led_all_control_store
。
-
设备文件:
- 每个 LED 设备对应一个设备文件
/dev/led0
,/dev/led1
,/dev/led2
。 - 对这些设备文件的读/写操作由
file_operations
中的函数(如led_write
)处理。
- 每个 LED 设备对应一个设备文件
类属性文件与设备文件的区别
-
类属性文件:
- 作用于设备类级别,可以对同类设备进行统一管理。
- 操作逻辑由
struct class_attribute
中的store
和show
函数实现。 - 在例子中,对
/sys/class/led_class/led_all_control
的写入调用了led_all_control_store
。
-
设备文件:
- 作用于具体的设备实例,可以对单个设备进行操作。
- 操作逻辑由
struct file_operations
中的函数(如read
、write
)实现。 - 在例子中,对
/dev/led0
的写入调用了led_write
。
代码中的调用关系
对类属性文件的写操作:
- 调用流程:
- 用户态:
write(fd, buffer, sizeof(buffer))
- 内核态:
led_all_control_store
- 用户态:
对设备文件的写操作:
- 调用流程:
- 用户态:
write(fd, buffer, sizeof(buffer))
(设备文件,如/dev/led0
) - 内核态:
led_write
- 用户态:
总结
- 写类属性文件
/sys/class/led_class/led_all_control
时,调用的是led_all_control_store
。 - 写设备文件
/dev/ledX
时,调用的是led_write
。 - 类属性文件适用于统一管理,设备文件适用于单个设备操作。
类属性文件中存储着什么信息?
类属性文件是 Linux 内核中的一种机制,用于通过 /sys/class/<class_name>/
目录中的属性文件与设备类相关的信息交互。这些文件通常由内核模块定义,用户空间可以通过读写这些文件与内核模块通信。
类属性文件的内容和作用
1. 存储的信息
类属性文件存储的信息取决于驱动开发者定义的逻辑。常见的内容包括:
- 设备状态(如 LED 是否打开)。
- 设备的配置信息(如模式、频率等)。
- 统计数据(如运行次数、错误计数等)。
- 控制指令(如启动、停止设备)。
类属性文件的存储信息是动态的,由类属性文件的读写回调函数(show
和 store
)定义,文件本身并没有固定内容。
2. 文件的读写方式
- 读取类属性文件:通过
cat /sys/class/<class_name>/<attr_name>
,调用class_attribute
中定义的show
回调函数获取信息。 - 写入类属性文件:通过
echo "value" > /sys/class/<class_name>/<attr_name>
,调用store
回调函数处理写入数据。
类属性文件的实现过程
以 LED 驱动为例:
定义类属性文件
// store函数 - 用于写操作
static ssize_t led_all_control_store(struct class *cls, struct class_attribute *attr, const char *buf, size_t count)
{int status, i;// 解析用户输入if (kstrtoint(buf, 10, &status) || (status != 0 && status != 1))return -EINVAL;// 设置所有 LED 的状态for (i = 0; i < LED_COUNT; i++)led_control(i, status);return count; // 返回写入的字节数
}// 定义类属性
CLASS_ATTR_WO(led_all_control);
添加类属性
ret = class_create_file(led_class, &class_attr_led_all_control);
此操作会在 /sys/class/led_class/
目录下创建文件 led_all_control
,绑定回调函数 led_all_control_store
。
类属性文件的用途
示例 1:通过类属性控制所有 LED
# 关闭所有 LED
echo 0 > /sys/class/led_class/led_all_control# 打开所有 LED
echo 1 > /sys/class/led_class/led_all_control
示例 2:通过类属性获取状态信息
如果类属性文件定义了 show
回调函数,例如:
static ssize_t led_all_control_show(struct class *cls, struct class_attribute *attr, char *buf)
{int i, status;status = led_status[0]; // 假设所有 LED 状态一致for (i = 1; i < LED_COUNT; i++) {if (led_status[i] != status) {return sprintf(buf, "mixed\n");}}return sprintf(buf, "%s\n", status ? "on" : "off");
}
可以通过以下命令获取状态:
cat /sys/class/led_class/led_all_control
小结
- 类属性文件的作用:提供一个简单的接口,让用户空间程序可以通过文件系统与设备类交互。
- 存储内容:动态生成,由开发者在
show
和store
回调函数中定义。 - 使用场景:常用于统一控制或查询某类设备的状态和配置。
相关文章:
以一个实际例子来学习Linux驱动程序开发之“设备类”的相关知识【利用设备类实现对同一设备类下的多个LED灯实现点亮或关闭】
前言 对于一个设备的驱动程序来说,其实上层用户主要看到的、用到的就是设备文件和设备类,当然用得最多的是设备文件,虽然设备类用得不多,但也是每一个设备注册实例化时必须要用到的东西,本篇博文就以一个简单的例子说…...
培训机构Day22
今天主要还是围绕着jquery讲解的,没有什么可说的。 知识点: 常用事件类型: 1.click:单击事件。鼠标,或键盘都可以触发。 2.dblclick:双击事件。 3.contextmenu:右键事件。 4.键盘相关事件&…...
Synopsys软件基本使用方法
Synopsys软件基本使用方法 1 文件说明2 编译流程3 查看波形4 联合仿真 本文主要介绍Synopsys软件vcs、verdi的基本使用方法,相关文件可从 GitHub下载。 1 文件说明 创建verilog源文件add.v、mult.v、top.vmodule add (input signed [31:0] dina,input signed [3…...
信息科技伦理与道德1:研究方法
1 问题描述 1.1 讨论? 请挑一项信息技术,谈一谈为什么认为他是道德的/不道德的,或者根据使用场景才能判断是否道德。判断的依据是什么(自身的道德准则)?为什么你觉得你的道德准则是合理的,其他…...
手机租赁平台开发实用指南与市场趋势分析
内容概要 在当今快速变化的科技时代,手机租赁平台的发展如火如荼。随着越来越多的人希望使用最新款的智能手机,但又不愿意承担昂贵的购机成本,手机租赁平台应运而生。这种模式不仅为用户提供了灵活的选择,还为企业创造了新的商机…...
ABAQUS三维Voronoi晶体几何建模
材料晶体塑性理论与细观尺度上晶体几何模型相融合的模拟方法为探究材料在塑性变形过程中的行为机制以及晶体材料优化开辟了新途径。本案例演示在CAD软件内通过Voronoi建立晶体三维模型,并将模型导入到Abaqus CAE内,完成晶体材料的有限元建模。 在AutoC…...
职场常用Excel基础04-二维表转换
大家好,今天和大家一起分享一下excel的二维表转换相关内容~ 在Excel中,二维表(也称为矩阵或表格)是一种组织数据的方式,其中数据按照行和列的格式进行排列。然而,在实际的数据分析过程中,我们常…...
如何使用 ChatGPT Prompts 写学术论文?
第 1 部分:学术写作之旅:使用 ChatGPT Prompts 进行学术写作的结构化指南 踏上学术写作过程的结构化旅程,每个 ChatGPT 提示都旨在解决特定方面,确保对您的主题进行全面探索。 制定研究问题: “制定一个关于量子计算的社会影响的研究问题,确保清晰并与您的研究目标保持一…...
【深度学习】Java DL4J基于 LSTM 构建新能源预测模型
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程,高并发设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s,热衷于探…...
Jetson系列部署YOLOv8模型教程
简介 NVIDIA Jetson系列是专为边缘计算设计的紧凑型计算模块,其目标用户为AI开发者、嵌入式系统工程师以及需要在设备端实时进行数据处理与AI推断的创新者。通过提供灵活的硬件平台,结合NVIDIA强大的GPU计算资源,Jetson系列能够支持复杂的机…...
【HAProxy】如何在Ubuntu下配置HAProxy服务器
HAProxy 是一款免费、开源且强大的反向代理程序,它为 HTTP 和 TCP 基础的应用提供了高可用性、负载均衡以及代理功能,因此对于管理高流量服务器(或 Web 应用)来说,通过将负载分散到多个节点服务器上,它是一…...
gesp(C++一级)(7)洛谷:B3863:[GESP202309 一级] 小明的幸运数
gesp(C一级)(7)洛谷:B3863:[GESP202309 一级] 小明的幸运数 题目描述 所有个位数为 k k k 的正整数,以及所有 k k k 的倍数,都被小明称为“ k k k 幸运数”。小明想知道正整数 L L L 和 R R R 之间&a…...
【数据库系统概论】数据库完整性与触发器--复习
在数据库系统概论中,数据库完整性是指确保数据库中数据的准确性、一致性和有效性的一组规则和约束。数据库完整性主要包括实体完整性、参照完整性和用户定义完整性。以下是详细的复习内容: 1. 数据库完整性概述 数据库完整性是指一组规则,这…...
【YOLOv8模型网络结构图理解】
YOLOv8模型网络结构图理解 1 YOLOv8的yaml配置文件2 YOLOv8网络结构2.1 Conv2.2 C3与C2f2.3 SPPF2.4 Upsample2.5 Detect层 1 YOLOv8的yaml配置文件 YOLOv8的配置文件定义了模型的关键参数和结构,包括类别数、模型尺寸、骨干(backbone)和头部…...
使用 commitlint 和 husky 检查提交描述是否符合规范要求
在上一小节中,我们了解了 Git hooks 的概念,那么接下来我们就使用 Git hooks 来去校验我们的提交信息。 要完成这么个目标,那么我们需要使用两个工具: 注意:npm 需要在 7.x 以上版本。 1. commitlint 用于检查提交信…...
QT集成IntelRealSense双目摄像头3,3D显示
前两篇文章,介绍了如何继承intel realsense相机和opengl。 这里介绍如何给深度数据和色彩数据一块显示到opengl里面。 首先,需要了解深度数据和彩色数据是如何存储的。先说彩色数据。彩色图像一般都是RGB,也就是每个像素有三个字节…...
Vue 中el-table-column 进行循环,页面没渲染成功
文章目录 前言效果图代码示例可能出现的问题及原因解决思路 前言 实现效果:el-table-column 进行循环,使之代码简化 遇到的问题: data进行默认赋值,操作列的删除都可以出来,其他表格里面的数据没出来 效果图 示例&am…...
渗透测试-非寻常漏洞案例
声明 本文章所分享内容仅用于网络安全技术讨论,切勿用于违法途径,所有渗透都需获取授权,违者后果自行承担,与本号及作者无关,请谨记守法. 此文章不允许未经授权转发至除先知社区以外的其它平台!࿰…...
Spring Boot 实战篇(四):实现用户登录与注册功能
目录 Spring Boot 实战篇(四):实现用户登录与注册功能 一、用户注册功能 (一)前端页面设计(简要提及) (二)后端实现 二、用户登录功能 (一)…...
VScode SSH 错误:Got bad result from install script 解決
之前vscode好好的,某天突然连接报错如下 尝试1. 服务器没有断开,ssh可以正常连接 2. 用管理员权限运行vscode,无效 3. 删除服务器上的~/.vscode-server 文件夹,无效 试过很多后,原来很可能是前一天anaconda卸载导致注册表项 步…...
openGauss与GaussDB系统架构对比
openGauss与GaussDB系统架构对比 系统架构对比openGauss架构GaussDB架构 GaussDB集群管理组件 系统架构对比 openGauss架构 openGauss是集中式数据库系统,业务数据存储在单个物理节点上,数据访问任务被推送到服务节点执行,通过服务器的高并…...
【ArcGISPro/GeoScenePro】检查并处理高程数据
数据 https://arcgis.com/sharing/rest/content/items/535efce0e3a04c8790ed7cc7ea96d02d/data 数字高程模型 (DEM) 是一种栅格,可显示地面或地形的高程。 数字表面模型 (DSM) 是另一种高程栅格,可显示表面的高度,例如建筑物或树冠的顶部。 您需要准备 DEM 和 DSM 以供分析…...
WebRTC的三大线程
WebRTC中的三个主要线程: signaling_thread,信号线程:用于与应用层交互worker_thread,工作线程(最核心):负责内部逻辑处理network_thread,网络线程:负责网络数据包的收发…...
HTML-文本标签
历史上,网页的主要功能是文本展示。所以,HTML 提供了大量的文本处理标签。 1.<div> <div>是一个通用标签,表示一个区块(division)。它没有语义,如果网页需要一个块级元素容器,又没…...
C# 在PDF中添加和删除水印注释 (Watermark Annotation)
目录 使用工具 C# 在PDF文档中添加水印注释 C# 在PDF文档中删除水印注释 PDF中的水印注释是一种独特的注释类型,它通常以透明的文本或图片形式叠加在页面内容之上,为文档添加标识或信息提示。与传统的静态水印不同,水印注释并不会永久嵌入…...
Unity2022接入Google广告与支付SDK、导出工程到Android Studio使用JDK17进行打包完整流程与过程中的相关错误及处理经验总结
注:因为本人也是第一次接入广告与支付SDK相关的操作,网上也查了很多教程,很多也都是只言片语或者缺少一些关键步骤的说明,导致本人也是花了很多时间与精力踩了很多的坑才搞定,发出来也是希望能帮助到其他人在遇到相似问…...
docker部署项目
docker部署项目 (加载tar包:docker image load -i mysql.tar) 一、jdk环境配置 1.jdk下载地址 --Java Archive | Oracle 中国 --选择好版本进入 --下载Linux x64 Compressed Archive的链接 2.解压 --创建文件夹:mkdir /ro…...
C# 设计模式(创建型模式):工厂模式
C# 设计模式(创建型模式):工厂模式 引言 在软件设计中,创建型模式是用来创建对象的设计模式,它们帮助我们将对象的创建过程从业务逻辑中分离出来,减少代码的重复性和耦合度。工厂模式作为创建型设计模式之…...
REMARK-LLM:用于生成大型语言模型的稳健且高效的水印框架
REMARK-LLM:用于生成大型语言模型的稳健且高效的水印框架 前言 提出这一模型的初衷为了应对大量计算资源和数据集出现伴随的知识产权问题。使用LLM合成类似人类的内容容易受到恶意利用,包括垃圾邮件和抄袭。 ChatGPT等大语言模型LLM的开发取得的进展标志着人机对话交互的范式…...
Lumos学习王佩丰Excel第二十三讲:Excel图表与PPT
一、双坐标柱形图的补充知识 1、主次坐标设置 2、主次坐标柱形避让(通过增加两个系列,挤压使得两个柱形挨在一起) 增加两个系列 将一个系列设置成主坐标轴,另一个设成次坐标轴 调整系列位置 二、饼图美化 1、饼图美化常见设置 …...
【Vue】v-if 和 :is 都是 Vue 中的指令,但它们用于不同的目的和场景
v-if v-if 是一个条件渲染指令,用于根据表达式的真假值来决定是否渲染一块内容。当 v-if 的表达式为真(truthy)时,Vue 会确保元素被渲染到 DOM 中;当表达式为假(falsy)时,元素不会被…...
private static final Logger log = LoggerFactory.getLogger()和@Slf4j的区别
一、代码方面 - private static final Logger log LoggerFactory.getLogger()方式 详细解释 这是一种传统的获取日志记录器(Logger)的方式。LoggerFactory是日志框架(如 Log4j、Logback 等)提供的工厂类,用于创建Lo…...
【项目】基于趋动云平台的Stable Diffusion开发
【项目】基于趋动云平台的Stable Diffusion开发 (一)登录趋动云(二)创建项目:(三)初始化开发环境:(四)运行代码(五)运行模型 …...
Git的.gitignore文件详解与常见用法
诸神缄默不语-个人CSDN博文目录 在日常使用 Git 进行版本控制时,我们经常会遇到一些不需要被提交到远程仓库的文件(例如日志文件、临时配置文件、环境变量文件等)。为了忽略这些文件的提交,Git 提供了一个非常有用的功能…...
客户端二维码优化居中和背景
原始 处理后...
Linux 安装运行gatk的教程
1.下载安装 wget https://github.com/broadinstitute/gatk/releases/download/4.1.8.1/gatk-4.1.8.1.zip2.解压 unzip *.zip3.查看 gatk --help 如下显示表示安装成功: 注意:仅限在该包所在位置的路径下能使用...
C++string类
1.为什么学习string类? 1.1C语言中的字符串 C语言中,字符串是以‘\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OO…...
下载linux aarch64版本的htop
htop代码网站似乎没有编译好的各平台的包,而自己编译需要下载一些工具,比较麻烦。这里找到了快速下载和使用的方法,记录一下。 先在linux电脑上执行: mkdir htop_exe cd htop_exe apt download htop:arm64 # 会直接下载到当前目…...
MYSQL---------支持数据类型
数值类型 整数类型 TINYINT:通常用于存储小范围的整数,范围是-128到127或0到255(无符号)。例如,存储年龄可以使用TINYINT类型。示例:CREATE TABLE users (age TINYINT);SMALLINT:范围比TINYINT…...
黑马JavaWeb开发跟学(十四).SpringBootWeb原理
黑马JavaWeb开发跟学 十四.SpringBootWeb原理 SpingBoot原理1. 配置优先级2. Bean管理2.1 获取Bean2.2 Bean作用域2.3 第三方Bean 3. SpringBoot原理3.1 起步依赖3.2 自动配置3.2.1 概述3.2.2 常见方案3.2.2.1 概述3.2.2.2 方案一3.2.2.3 方案二 3.2.3 原理分析3.2.3.1 源码跟踪…...
迅为RK3568开发板编译Android12源码包-设置屏幕配置
在源码编译之前首先要确定自己想要使用的屏幕并修改源码,在编译镜像,烧写镜像。如下图所示: 第一步:确定要使用的屏幕种类,屏幕种类选择如下所示: iTOP-3568 开发板支持以下种类屏幕: 迅为 LV…...
Spring Boot 中 TypeExcludeFilter 的作用及使用示例
在Spring Boot应用程序中,TypeExcludeFilter 是一个用于过滤特定类型的组件,使之不被Spring容器自动扫描和注册为bean的工具。这在你想要排除某些类或类型(如配置类、组件等)而不希望它们参与Spring的自动装配时非常有用。 作用 …...
Prometheus 采集 JVM 数据
Prometheus 采集 JVM 数据通常通过集成 JMX Exporter(Java Management Extensions Exporter)实现。以下是完整的介绍和操作步骤: 1. 原理概述 JVM 数据采集依赖于 JMX(Java Management Extensions),JVM 提…...
OpenNJet v3.2.0正式发布!
在这个版本中,NJet实现重大突破,提供了动态Upstream的能力。这是一个关键的特性,使得NJet可以按需动态管理上游服务器池,从而使得业务方可以按需配置资源池,实现业务分区、算法切换;结合动态路由技术&#…...
TinaCMS: 革命性的开源内容管理框架
在如今的数字时代,高效的内容管理系统(CMS)已成为构建内容丰富网站和应用程序的必需品。传统 CMS,如 WordPress 和 Drupal,功能丰富但复杂度高。而新一代 CMS,例如 TinaCMS,以其灵活性和开发者友…...
VuePress2配置unocss的闭坑指南
文章目录 1. 安装依赖:准备魔法材料2. 检查依赖版本一定要一致:确保魔法配方准确无误3. 新建uno.config.js:编写咒语书4. 配置config.js和client.js:完成仪式 1. 安装依赖:准备魔法材料 在开始我们的前端魔法之前&…...
SpringCloud(二)--SpringCloud服务注册与发现
一. 引言 前文简单介绍了SpringCloud的基本简介与特征,接下来介绍每个组成部分的功能以及经常使用的中间件。本文仅为学习所用,联系侵删。 二. SpringCloud概述 2.1 定义 Spring Cloud是一系列框架的有序集合,它巧妙地利用了Spring…...
JavaVue-Get请求 数组参数(qs格式化前端数据)
前言 现在管理系统,像若依,表格查询一般会用Get请求,把页面的查询条件传递给后台。其中大部分页面会有日期时间范围查询这时候,为了解决请求参数中的数组文件,前台就会在请求前拦截参数中的日期数组数据,然…...
Java-多种方法实现多线程卖票
Java多线程卖票是一个经典的并发编程问题,它展示了如何在多个线程之间安全地共享和修改资 源。以下是几种实现方式: 使用synchronized关键字: 使用synchronized修饰符来同步方法或代码块,确保同一时刻只有一个线程可以访问临界区(即操 作共享资源的代码)。 使用Reen…...
LLVM防忘录
目录 Windows中源码编译LLVMWindows下编译LLVM Pass DLL Windows中源码编译LLVM 直接从llvm-project下载源码, 然后解压后用VS2022打开该目录, 然后利用VS的开发终端执行: cmake -S llvm -B build -G "Visual Studio 17 2022" -DLLVM_ENABLE_PROJECTSclang -DLLVM_…...