以一个实际例子来学习Linux驱动程序开发之“设备类”的相关知识【利用设备类实现对同一设备类下的多个LED灯实现点亮或关闭】

news2025/1/6 22:58:10

前言

对于一个设备的驱动程序来说,其实上层用户主要看到的、用到的就是设备文件和设备类,当然用得最多的是设备文件,虽然设备类用得不多,但也是每一个设备注册实例化时必须要用到的东西,本篇博文就以一个简单的例子说明设备类的功能。

设备类的本质

所谓设备类,本质上就是“物以类聚,人以群分”思想的体现,它允许每个设备有一个自己的所属类,说白了就是所属分组,假如某几个设备的所属类是相同的,那么我们就能对这些设备进行一些统一的操作。

下面以一个实际例子看下设备类在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;

    // 控制对应的 LED
    led_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);

    // 删除 cdev
    cdev_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);

    // 删除 cdev
    cdev_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() {
    // 点亮所有 LED
    control_leds(1);

    // 延时 2 秒
    sleep(2);

    // 关闭所有 LED
    control_leds(0);

    return 0;
}

程序说明

  1. 路径定义: LED_CLASS_ATTR_PATH 定义了类属性文件路径,需与驱动生成的路径一致。
  2. 文件操作: 使用 open 打开类属性文件,使用 write 将状态值写入文件。
  3. LED 控制: 程序调用 control_leds 函数,参数为 1 表示点亮,0 表示关闭。

输出结果

运行程序后,可以观察到:

  • 所有 LED 点亮,延时 2 秒后关闭。
  • 控制状态在 /sys/class/led_class/led_all_control 中生效,同时会打印相应的日志信息。

注意事项

  1. 确保 /sys/class/led_class/led_all_control 文件存在。
  2. 如果遇到权限问题,可以手动修改类属性文件权限:
    chmod 666 /sys/class/led_class/led_all_control
    
  3. 如果需要更复杂的控制逻辑,可以扩展 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

原因分析

  1. 类属性文件:

    • /sys/class/led_class/led_all_control 是通过 class_create_file 创建的类属性文件。
    • 类属性文件操作(如读/写)由相应的 storeshow 函数处理,在例子中是 led_all_control_store
  2. 设备文件:

    • 每个 LED 设备对应一个设备文件 /dev/led0, /dev/led1, /dev/led2
    • 对这些设备文件的读/写操作由 file_operations 中的函数(如 led_write)处理。

类属性文件与设备文件的区别

  • 类属性文件:

    • 作用于设备类级别,可以对同类设备进行统一管理。
    • 操作逻辑由 struct class_attribute 中的 storeshow 函数实现。
    • 在例子中,对 /sys/class/led_class/led_all_control 的写入调用了 led_all_control_store
  • 设备文件:

    • 作用于具体的设备实例,可以对单个设备进行操作。
    • 操作逻辑由 struct file_operations 中的函数(如 readwrite)实现。
    • 在例子中,对 /dev/led0 的写入调用了 led_write

代码中的调用关系

对类属性文件的写操作:
  • 调用流程:
    1. 用户态:write(fd, buffer, sizeof(buffer))
    2. 内核态:led_all_control_store
对设备文件的写操作:
  • 调用流程:
    1. 用户态:write(fd, buffer, sizeof(buffer))(设备文件,如 /dev/led0
    2. 内核态:led_write

总结

  • 写类属性文件 /sys/class/led_class/led_all_control 时,调用的是 led_all_control_store
  • 写设备文件 /dev/ledX 时,调用的是 led_write
  • 类属性文件适用于统一管理,设备文件适用于单个设备操作。

类属性文件中存储着什么信息?

类属性文件是 Linux 内核中的一种机制,用于通过 /sys/class/<class_name>/ 目录中的属性文件与设备类相关的信息交互。这些文件通常由内核模块定义,用户空间可以通过读写这些文件与内核模块通信。


类属性文件的内容和作用

1. 存储的信息

类属性文件存储的信息取决于驱动开发者定义的逻辑。常见的内容包括:

  • 设备状态(如 LED 是否打开)。
  • 设备的配置信息(如模式、频率等)。
  • 统计数据(如运行次数、错误计数等)。
  • 控制指令(如启动、停止设备)。

类属性文件的存储信息是动态的,由类属性文件的读写回调函数(showstore)定义,文件本身并没有固定内容。

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

小结

  • 类属性文件的作用:提供一个简单的接口,让用户空间程序可以通过文件系统与设备类交互。
  • 存储内容:动态生成,由开发者在 showstore 回调函数中定义。
  • 使用场景:常用于统一控制或查询某类设备的状态和配置。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2271159.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【新教程】华为昇腾NPU的pytorch环境搭建

1 硬件配置 使用学校的集群&#xff0c;相关配置如下&#xff1a; CPU&#xff1a;鲲鹏920 NPU&#xff1a;昇腾910B 操作系统&#xff1a;openEuler 22.03 2 安装版本 根据昇腾官方gitee上的信息&#xff0c;Pytoch 2.1.0是长期支持版本&#xff0c;因此选择安装这一版本&a…

游戏引擎学习第72天

无论如何&#xff0c;我们今天有一些调试工作要做&#xff0c;因为昨天做了一些修改&#xff0c;结果没有时间进行调试和处理。我们知道自己还有一些需要解决的问题&#xff0c;却没有及时完成&#xff0c;所以我们想继续进行这些调试。对我们来说&#xff0c;拖延调试工作总是…

信号的产生、处理

一、信号的概念 信号是linux系统提供的一种&#xff0c;向指定进程发送特定事件的方式。收到信号的进程&#xff0c;要对信号做识别和处理。信号的产生是异步的&#xff0c;进程在工作过程中随时可能收到信号。 信号的种类分为以下这么多种&#xff08;用指令kill -l查看&…

Node.js应用程序遇到了内存溢出的问题

vue 项目 跑起来&#xff0c;一直报错&#xff0c;内存溢出 在 文件node_modules 里 .bin > vue-cli-service.cmd 在依赖包这个文件第一行加上这个 node --max-old-space-size102400 "%~dp0\..\vue\cli-service\bin\vue-cli-service.js" %* node --max-old-s…

openGauss与GaussDB系统架构对比

openGauss与GaussDB系统架构对比 系统架构对比openGauss架构GaussDB架构 GaussDB集群管理组件 系统架构对比 openGauss架构 openGauss是集中式数据库系统&#xff0c;业务数据存储在单个物理节点上&#xff0c;数据访问任务被推送到服务节点执行&#xff0c;通过服务器的高并…

深入理解计算机系统—虚拟内存(一)

一个系统中的进程是与其他进程共享 CPU 和主存资源的。然而&#xff0c;共享主存会形成特殊的挑战。随着对 CPU 需求的增长&#xff0c;进程以某种合理的平滑方式慢了下来。但是如果太多的进程需要太多的内存&#xff0c;那么它们中的一些就根本无法运行。 为了更加有效地管理内…

九、Vue 事件处理器

文章目录 前言一、基础事件绑定:v-on 指令二、方法调用:组织有序的交互逻辑三、事件修饰符阻止冒泡与默认事件捕获与自身触发单次触发与鼠标按键区分四、按键修饰符前言 在 Vue.js 的交互世界里,事件处理器起着举足轻重的作用,它让页面从静态展示迈向动态交互,精准捕捉用户…

Quartus In-System Sources and Probes Editor 的使用说明

文章目录 前言使用说明参考资料 前言 Quartus 提供了 In-System Sources and Probes Editor 调试工具&#xff0c;通过 JTAG 接口使用该工具可以驱动和采样内部节点的逻辑值。即通过 Sources 功能来驱动 FPGA 内部信号&#xff0c;通过 Probes 功能来探测内部节点的逻辑值。在…

springboot整合Quartz实现定时任务

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言1.核心概念2.基础实现2.1引入依赖2.2创建具体逻辑类2.3配置类 总结 前言 在项目中我们会有许多要进行定时执行逻辑的业务场景&#xff0c;比如定期生成日报、定…

阿里云 ECS 服务器绑定多个公网IP

阿里云 ECS 服务器绑定多个公网IP 一、弹性公网IP绑定ECS服务器 单台ECS一般只能直接绑定一个弹性公网IP&#xff0c;但是可以绑定多张弹性网卡&#xff0c;如果把弹性公网IP绑定到弹性网卡上&#xff0c;那么单台ECS就能间接绑定多个弹性公网IP。但有的服务器系统镜像可能不…

线性代数考研笔记

行列式 背景 分子行列式&#xff1a;求哪个未知数&#xff0c;就把b1&#xff0c;b2放在对应的位置 分母行列式&#xff1a;系数对应写即可 全排列与逆序数 1 3 2&#xff1a;逆序数为1 奇排列 1 2 3&#xff1a;逆序数为0 偶排列 将 1 3 2 只需将3 2交换1次就可以还原原…

LLM - 使用 LLaMA-Factory 部署大模型 HTTP 多模态服务 (4)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/144881432 大模型的 HTTP 服务&#xff0c;通过网络接口&#xff0c;提供 AI 模型功能的服务&#xff0c;允许通过发送 HTTP 请求&#xff0c;交互…

数据库知识汇总2

一. 范式 定义&#xff1a;范式是符合某一种级别的关系模式的集合。 关系数据库中的关系必须满足一定的要求。满足不同程度要求的为不同范式&#xff1b; 一个低一级范式的关系模式&#xff0c;通过模式分解&#xff08;schema decomposition&#xff09;可以转换为若干个高一…

TP 钱包插件版本的使用

目前 TokenPocket 的几个平台中&#xff0c;以 ios 和 安卓版本最为常见&#xff0c;其实很少有人知道&#xff0c;浏览器上有一个插件版本的 Tp, 用电脑多的话&#xff0c;这也是一个挺好的选择。 最新版本现在支持Chrome、Brave 浏览器、Edge&#xff08;Firefox及Opera正在…

反向传播算法的偏置更新步骤

偏置的更新步骤 假设我们有一个三层神经网络&#xff08;输入层、隐藏层和输出层&#xff09;&#xff0c;并且每层的激活函数为 sigmoid 函数。我们需要更新隐藏层和输出层的偏置。以下是详细的步骤&#xff1a; 1. 计算误差项&#xff08;Error Term&#xff09; 输出层的…

(二)当人工智能是一个函数,函数形式怎么选择?ChatGPT的函数又是什么?

在上一篇文章中&#xff0c;我们通过二次函数的例子&#xff0c;讲解了如何训练人工智能。今天&#xff0c;让我们进一步探讨&#xff1a;面对不同的实际问题&#xff0c;应该如何选择合适的函数形式&#xff1f; 一、广告推荐系统中的函数选择 1. 业务目标 想象一下&#x…

Vue3 中的插槽

Vue3 中插槽的使用&#xff0c;插槽是 Vue 中的一个特别特性&#xff0c;插槽就是模版内容。例如<h1>标题 1</h1>标题 1 就是插槽&#xff0c;Vue 是无法识别模板内容的&#xff0c;只能通过属性进行传递。Slot 主要包括默认、具名和作用域。Slot开发起来难度不大&…

单元测试3.0+ @RunWith(JMockit.class)+mock+injectable+Expectations

Jmockit使用笔记_基本功能使用Tested_Injectable_Mocked_Expectations_jmockit.class-CSDN博客 静态变量直接赋值就好&#xff0c;没必要mock了 测试框架Jmockit集合junit使用 RunWith(JMockit.class) 写在测试案例类上的注解 Tested 在测试案例中,写在我们要测试的类上…

靶机系列|VULNHUB|DC-3

描述 DC-3 是另一个专门建造的易受攻击实验室&#xff0c;旨在获得渗透测试领域的经验。 与之前的 DC 版本一样&#xff0c;这个版本的设计考虑到了初学者&#xff0c;尽管这一次只有一个标志、一个入口点&#xff0c;根本没有线索。 必须具备 Linux 技能和熟悉 Linux 命令行…

sqlserver sql转HTMM邮件发送

通过sql的形式&#xff0c;把表内数据通过邮件的形式发送出去 declare title varchar(100) DECLARE stat_date CHAR(10),create_time datetime SET stat_dateCONVERT(char(10),GETDATE(),120) SET create_timeDATEADD(MINUTE,-20,GETDATE()) DECLARE xml NVARCHAR (max) DECLAR…