以一个实际例子来学习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
回调函数中定义。 - 使用场景:常用于统一控制或查询某类设备的状态和配置。