当前位置: 首页 > news >正文

Linux中的LED子专业的系统

主要参考:

Linux Led 子系统-腾讯云开发者社区-腾讯云

LED 子系统简介

Linux 中的 LED 子系统(LED Subsystem) 是内核专门为 LED 设备设计的标准化框架,用于统一管理各类 LED 硬件(如 GPIO 控制的 LED、PWM 调光 LED、专用芯片驱动的 LED 等),并为用户空间提供一致的控制接口。

一、LED 子系统的核心目标

  1. 统一驱动模型:无论 LED 由何种硬件控制(GPIO、PWM、I2C 芯片等),都通过相同的内核接口抽象,简化驱动开发。
  2. 标准化用户接口:自动在 sysfs 中创建控制节点(如 /sys/class/leds/),用户空间无需关心硬件细节,直接通过文件操作控制 LED。
  3. 支持通用功能:内置亮度调节、闪烁模式、触发机制(如 “心跳”“定时器”)等,避免重复开发。

二、核心数据结构:struct led_classdev

led_classdev 是 LED 子系统的核心结构体,封装了 LED 设备的属性和操作方法,定义在 include/linux/leds.h 中:

struct led_classdev {
const char      *name;               // LED 名称(对应 /sys/class/leds/ 下的目录名)
enum led_brightness brightness;      // 当前亮度(0 表示灭,255 表示最亮)
enum led_brightness max_brightness;  // 最大亮度(通常为 255)
// 设置亮度的回调函数(驱动必须实现)
void (*brightness_set)(struct led_classdev *led_cdev, enum led_brightness brightness);
// 获取亮度的回调函数(可选)
enum led_brightness (*brightness_get)(struct led_classdev *led_cdev);
// 其他成员:闪烁控制、触发模式、设备指针等
};
  • 亮度值enum led_brightness 定义了亮度范围,LED_OFF(0)表示熄灭,LED_FULL(255)表示最亮,中间值可用于调光。
  • 核心回调brightness_set 是驱动的核心函数,负责将用户设置的亮度值(如 255 或 0)转换为硬件操作(如 GPIO 电平切换、PWM 占空比调节)。

三、LED 子系统的工作流程

1. 驱动开发流程

(1)定义并初始化 led_classdev

 
static struct led_classdev my_led = {
.name = "my_led",                // 设备名称,对应 /sys/class/leds/my_led/
.max_brightness = LED_FULL,      // 最大亮度 255
.brightness_set = my_led_set,    // 自定义亮度设置函数
};

(2)实现 brightness_set 回调

 
static void my_led_set(struct led_classdev *cdev, enum led_brightness brightness) {
// 假设 LED 连接到 GPIO10,高电平点亮
if (brightness == LED_OFF) {
gpio_set_value(10, 0);  // 低电平→灭
} else {
gpio_set_value(10, 1);  // 高电平→亮
}
}

(3)注册 LED 设备
通过 led_classdev_register 注册设备,内核会自动在 sys/class/leds/ 下创建控制节点:

 
int ret = led_classdev_register(dev, &my_led);
if (ret) {
dev_err(dev, "LED 注册失败\n");
return ret;
}

2. 用户空间控制接口

注册成功后,sys/class/leds/my_led/ 目录下会生成以下关键文件:

  • brightness:读写当前亮度(0 灭,255 亮)。
    echo 255 > /sys/class/leds/my_led/brightness  # 点亮
    echo 0 > /sys/class/leds/my_led/brightness    # 熄灭
  • max_brightness:只读,返回最大亮度(通常为 255)。
  • trigger:设置 LED 触发模式(如 “心跳”“定时器”“键盘事件” 等)。
    echo heartbeat > /sys/class/leds/my_led/trigger  # 心跳模式(亮灭交替)
    echo none > /sys/class/leds/my_led/trigger       # 取消触发模式

四、典型应用场景

  1. GPIO 控制的 LED:最常见的场景,通过 GPIO 电平切换控制亮灭(如开发板上的用户 LED)。内核已提供通用驱动 drivers/leds/leds-gpio.c,可直接通过设备树配置使用。
  2. PWM 调光 LED:支持亮度渐变(如背光调节),通过 PWM 占空比控制亮度,对应驱动 drivers/leds/leds-pwm.c
  3. 触发模式:无需用户手动控制,LED 自动响应系统事件(如 “心跳” 模式指示系统运行,“disk-activity” 模式指示磁盘读写)。

五、优势与意义

  1. 简化驱动开发:驱动只需实现硬件相关的 brightness_set 函数,其他通用功能(如 sysfs 节点创建、触发模式)由子系统自动处理。
  2. 用户友好:提供统一的 sysfs 接口,用户空间(应用程序、脚本)可直接控制,无需适配不同硬件。
  3. 可扩展性:支持多 LED 实例、动态亮度调节、低功耗模式等,满足产品级需求。

总结

LED 子系统是 Linux 内核中管理 LED 设备的标准化框架,通过 led_classdev 结构体抽象硬件差异,提供统一的驱动接口和用户空间控制方式。无论是简单的 GPIO LED 还是复杂的 PWM 调光 LED,都能通过该子系统快速实现符合内核规范的驱动,是产品级 LED 驱动开发的首选方案。

使用场景

在 Linux 系统中,是否直接使用内核自带的 LED 驱动(如 leds-gpio.cleds-pwm.c 等),还是需要自行编写驱动,主要取决于 硬件特性 和 功能需求。以下是具体判断标准:

一、可以直接使用内核自带 LED 驱动的情况

当 LED 设备满足 “硬件简单、功能通用、符合内核驱动适配范围” 时,无需自定义驱动,直接使用内核自带驱动即可,典型场景包括:

  • GPIO 直接控制的普通 LED

    • 硬件特点:LED 通过单个 GPIO 引脚控制(高 / 低电平点亮 / 熄灭),无特殊时序或外部电路依赖。
    • 适用驱动:leds-gpio.c(内核主线驱动)。
    • 使用方式:通过设备树配置 GPIO 引脚、有效电平(高 / 低)、默认状态等,驱动会自动匹配并生成 /sys/class/leds/ 控制接口。
  • PWM 调光的 LED

    • 硬件特点:LED 亮度通过 PWM 信号占空比调节(如背光 LED),依赖内核 PWM 子系统。
    • 适用驱动:leds-pwm.c(内核主线驱动)。
    • 使用方式:设备树中指定 PWM 控制器、通道及频率,驱动自动实现亮度与占空比的映射。
  • 支持标准触发模式的场景

    • 功能需求:仅需基础功能(亮 / 灭、亮度调节)或内核默认触发模式(如 heartbeat 心跳、timer 定时闪烁、disk-activity 磁盘活动指示)。
    • 优势:无需编写代码,通过 sysfs 接口(如 /sys/class/leds/xxx/trigger)即可配置。
  • 硬件符合通用驱动的适配范围

    • 若 LED 由内核已支持的专用芯片(如 I2C 接口的 PCA955x 系列 LED 控制器)驱动,且功能无需扩展,可直接使用对应的芯片驱动(如 leds-pca955x.c)。

二、需要自行编写 LED 驱动的情况

当 LED 设备 “硬件复杂、功能特殊”,或通用驱动无法满足产品需求时,需自定义驱动,典型场景包括:

  • 硬件控制方式特殊

    • 非 GPIO/PWM 控制:如通过 SPI 接口的 LED 矩阵、需要特定时序的 RGB LED 驱动芯片(如 WS2812),通用驱动无法适配硬件协议。
    • 复杂电路依赖:LED 需配合电源管理芯片、过流保护电路或多级放大电路工作,需在驱动中集成硬件初始化和状态检测逻辑。
  • 功能需求超出通用驱动范围

    • 自定义调光逻辑:如非线性亮度曲线(gamma 校正)、基于环境光传感器的自动亮度调节,需重写 brightness_set 回调函数。
    • 复杂联动效果:如多 LED 同步实现流水灯、追逐动画,或 LED 与其他设备(如按键、传感器)的联动响应,通用驱动的单设备管理模式无法满足。
    • 自定义触发模式:内核默认触发模式(如心跳、定时)无法满足需求(如 Morse 码闪烁、随音频波形变化),需在驱动中实现自定义触发逻辑。
  • 产品级特性需求

    • 电源管理优化:需在系统休眠 / 唤醒时精确控制 LED 状态(如休眠时关闭以省电,唤醒后恢复原亮度),需自定义 suspend/resume 回调。
    • 权限与安全控制:LED 用于敏感场景(如工业报警灯),需限制用户空间访问权限(如仅 root 可操作),通用驱动的默认权限管理不足。
    • 诊断与日志:需通过 sysfs/debugfs 暴露硬件状态(如当前功耗、温度)或记录操作日志,便于问题排查。
  • 硬件或内核版本不兼容

    • 旧内核适配:设备运行的内核版本过旧(如 3.x 以前),通用驱动缺失设备树支持或关键功能,需编写兼容旧接口的驱动。
    • 特殊架构:非标准硬件平台(如专用 DSP)的 GPIO/PWM 控制器与通用驱动不兼容,需针对硬件重写控制逻辑。

总结

  • 优先使用内核自带驱动:简单 GPIO/PWM 控制的 LED,功能需求为基础亮灭、调光或默认触发模式,硬件符合通用驱动适配范围。
  • 必须自定义驱动:硬件控制方式特殊(非 GPIO/PWM、复杂电路)、功能需求超出通用范围(自定义调光 / 联动 / 触发),或需满足产品级特性(电源管理、权限控制等)。

自定义驱动时,建议基于 LED 子系统(led_classdev)开发,以保持与内核框架的兼容性,仅聚焦于硬件交互和扩展功能的实现。

LED子系统框架

led 子系统驱动框架:

对应的目录文件如下:

led 子系统核心文件:

driver/leds/led-class.c
driver/leds/led-core.c
driver/leds/led-triggers.c
include/linux/leds.h

其他文件(按需)

driver/leds/led-gpio.c
driver/leds/wm8350.c
driver/leds/led-xxx.c
driver/leds/trigger/ledtrig-backlight.c
driver/leds/trigger/ledtrig-camera.c
driver/leds/trigger/ledtrig-cpu.c
driver/leds/trigger/ledtrig-default-on.c
driver/leds/trigger/ledtrig-gpio.c
driver/leds/trigger/ledtrig-heartbeat.c
driver/leds/trigger/ledtrig-ide-disk.c
driver/leds/trigger/ledtrig-multi-control.c
driver/leds/trigger/ledtrig-oneshot.c
driver/leds/trigger/ledtrig-timer.c
driver/leds/trigger/ledtrig-transient.c

led 子系统相关描述可在内核源码 Documentation/leds/leds-class.txt 了解。

代码框架分析

led-class.c (led 子系统框架的入口)

维护 LED 子系统的所有 LED 设备,为 LED 设备提供注册操作函数:
led_classdev_register()
devm_led_classdev_register()
注销操作函数:
led_classdev_unregister()
devm_led_classdev_unregister();
电源管理的休眠和恢复操作函数:
led_classdev_suspend()
led_classdev_resume();
用户态操作接口:brightness 、max_brightness

led-core.c

抽象出 LED 操作逻辑,封装成函数导出,供其它文件使用:
led_init_core(): 核心初始化;
led_blink_set(): 设置led闪烁时间:
led_blink_set_oneshot() : 闪烁一次
led_stop_software_blink() : led停止闪烁
led_set_brightness() : 设置led的亮度
led_update_brightness : 更新亮度
led_sysfs_disable : 用户态关闭
led_sysfs enable : 用户态打开
leds_list : leds链表;
leds_list_lock : leds链表锁

led-triggers.c

维护 LED 子系统的所有触发器,为触发器提供注册操作函数:
led_trigger_register()
devm_led_trigger_register()
led_trigger_register_simple()
注销操作函数:
led_trigger_unregister()
led_trigger_unregister_simple()
以及其它触发器相关的操作函数

ledtrig-timer.c、ledtrig-xxx.c

以 ledtrig-timer.c 为例
入口函数调用 led_trigger_register() 注册触发器,
注册时候传入 led_trigger 结构体,里面有 activate 和 deactivate 成员函数指针,
作用是生成 delay_on 、 delay_off 文件
同时还提供 delay_on 和 delay_off 的用户态操作接口
卸载时,使用 led_trigger_unregister() 注销触发器

leds-gpio.c、leds-xxx.c :

以 leds-gpio.c 为例
在通过设备树或者其它途径匹配到设备信息后,将调用 probe() 函数,
然后再根据设备信息设置 led_classdev,
最后调用 devm_led_classdev_register() 注册 LED 设备。

led_classdev 结构体代表 led 实例:

struct led_classdev {
const char  *name;//名字
enum led_brightness  brightness;//亮度
enum led_brightness  max_brightness;//最大亮度
int    flags;
/* Lower 16 bits reflect status */
#define LED_SUSPENDED  (1 << 0)
/* Upper 16 bits reflect control information */
#define LED_CORE_SUSPENDRESUME (1 << 16)
#define LED_BLINK_ONESHOT (1 << 17)
#define LED_BLINK_ONESHOT_STOP (1 << 18)
#define LED_BLINK_INVERT (1 << 19)
#define LED_SYSFS_DISABLE (1 << 20)
#define SET_BRIGHTNESS_ASYNC (1 << 21)
#define SET_BRIGHTNESS_SYNC (1 << 22)
#define LED_DEV_CAP_FLASH (1 << 23)
//设置亮度API
void  (*brightness_set)(struct led_classdev *led_cdev,enum led_brightness brightness);
int  (*brightness_set_sync)(struct led_classdev *led_cdev,enum led_brightness brightness);
//获取亮度API
enum led_brightness (*brightness_get)(struct led_classdev *led_cdev);
//闪烁时点亮和熄灭的时间设置
int  (*blink_set)(struct led_classdev *led_cdev,unsigned long *delay_on,unsigned long *delay_off);
struct device  *dev;
const struct attribute_group **groups;
//leds-list的node
struct list_head  node;
//默认trigger的名字
const char  *default_trigger;
//闪烁的开关时间
unsigned long   blink_delay_on, blink_delay_off;
//闪烁的定时器链表
struct timer_list  blink_timer;
//闪烁的亮度
int    blink_brightness;
void   (*flash_resume)(struct led_classdev *led_cdev);
struct work_struct set_brightness_work;
int   delayed_set_value;
#ifdef CONFIG_LEDS_TRIGGERS
//trigger的锁
struct rw_semaphore  trigger_lock;
//led的trigger
struct led_trigger *trigger;
//trigger的链表
struct list_head  trig_list;
//trigger的数据
void   *trigger_data;
bool   activated;
#endif
struct mutex  led_access;
};

led_trigger 结构:

struct led_trigger {
/* Trigger Properties */
const char  *name;
void  (*activate)(struct led_classdev *led_cdev);
void  (*deactivate)(struct led_classdev *led_cdev);
/* LEDs under control by this trigger (for simple triggers) */
rwlock_t   leddev_list_lock;
struct list_head  led_cdevs;
/* Link to next registered trigger */
struct list_head  next_trig;
};

trigger 是控制 LED 类设备的算法,这个算法决定着 LED 什么时候亮什么时候暗。

注意:

leds-gpio.c 是 Linux 内核中一个具体的驱动程序,而非框架。它是基于 LED 子系统(LED Subsystem)框架实现的、专门用于控制 GPIO 连接的 LED 设备的驱动。

明确概念:框架 vs 驱动

  • LED 子系统(框架):是内核提供的一套标准化接口和机制(核心是 struct led_classdev 结构体及相关函数),定义了 LED 设备的抽象模型、用户空间接口(sysfs)和驱动开发规范。它是 “骨架”,不直接操作硬件,而是提供通用能力。

  • leds-gpio.c(驱动):是基于 LED 子系统框架编写的具体实现,负责将框架的抽象接口与实际硬件(GPIO 控制的 LED)对接。它实现了 LED 子系统要求的回调函数(如 brightness_set),并通过 GPIO 子系统操作硬件引脚,完成 “亮 / 灭”“亮度调节” 等具体功能。

leds-gpio.c 的核心作用

  1. 硬件适配:专门处理通过 GPIO 引脚控制的 LED(最常见的 LED 硬件形态),将 GPIO 电平操作(高 / 低电平)与 LED 子系统的亮度值(0~255)对应起来。
  2. 设备树解析:通过设备树获取 LED 对应的 GPIO 引脚、有效电平(高 / 低)、默认状态等硬件信息,无需硬编码。
  3. 标准化接口:基于 LED 子系统框架,自动创建 /sys/class/leds/ 下的控制节点,让用户空间可以通过统一的 brightnesstrigger 等文件控制 LED。

总结

  • LED 子系统是框架,提供通用规范和接口;
  • leds-gpio.c 是基于该框架的具体驱动,负责 GPIO 类型 LED 的硬件控制。

类似地,内核中还有 leds-pwm.c(PWM 调光 LED 驱动)、leds-pca955x.c(I2C 芯片控制的 LED 驱动)等,它们都是基于 LED 子系统框架实现的具体驱动,分别适配不同硬件类型的 LED。

leds-gpio.c驱动实现

文件如下:

首先,该驱动使用了platform框架

可以看到,该驱动框架提供了基础的probe和remove函数,并且指定了驱动的name字段以及of_match_table匹配表。

当驱动和设备匹配时,就会触发probe函数

接下来重点关注下这个函数

这段代码的核心逻辑是根据设备信息(平台数据或设备树)创建 LED 设备实例,并完成硬件初始化。

代码逐段解析:

1. 变量定义与平台数据获取

struct gpio_led_platform_data *pdata = dev_get_platdata(&pdev->dev);
struct gpio_leds_priv *priv;
int i, ret = 0;
  • pdata:获取平台设备的私有数据(struct gpio_led_platform_data 类型),包含 LED 数量、每个 LED 的配置等信息。
  • priv:驱动私有数据结构体指针(struct gpio_leds_priv),用于存储驱动运行时的状态(如 LED 数量、每个 LED 的具体数据)。

2. 处理传统平台数据(非设备树)的情况

if (pdata && pdata->num_leds) {
priv = devm_kzalloc(&pdev->dev,
sizeof_gpio_leds_priv(pdata->num_leds),
GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->num_leds = pdata->num_leds;
for (i = 0; i num_leds; i++) {
ret = create_gpio_led(&pdata->leds[i],
&priv->leds[i],
&pdev->dev, pdata->gpio_blink_set);
if (ret = 0; i--)
delete_gpio_led(&priv->leds[i]);
return ret;
}
}
}
  • 条件判断:当设备通过传统平台数据(非设备树)配置时(pdata 存在且包含 LED 数量),进入此分支。
  • 内存分配:通过 devm_kzalloc 分配 priv 结构体内存,其中包含一个柔性数组 leds[],大小由 sizeof_gpio_leds_priv 计算(结构体固定部分 + 每个 LED 数据的大小 × 数量)。
  • 初始化每个 LED:循环调用 create_gpio_led 初始化每个 LED,包括申请 GPIO 资源、注册 LED 设备到系统等。
  • 错误处理:若某个 LED 初始化失败,回滚已创建的 LED(调用 delete_gpio_led),避免资源泄漏。

3. 处理设备树配置的情况

else {
priv = gpio_leds_create(pdev);
if (IS_ERR(priv))
return PTR_ERR(priv);
}
  • 条件判断:当没有传统平台数据时(通常是设备树配置的情况),进入此分支。
  • 从设备树创建 LEDgpio_leds_create 函数会解析设备树中 LED 相关的节点(如 gpiolabeldefault-trigger 等属性),并完成与上述分支类似的初始化工作。
  • 错误处理:若设备树解析或初始化失败,通过 IS_ERR 判断错误并返回。

4. 保存私有数据并返回

platform_set_drvdata(pdev, priv);
return 0;
  • platform_set_drvdata:将 priv 指针与平台设备绑定,方便后续在 remove 等函数中通过 platform_get_drvdata 获取。
  • 返回 0 表示 probe 成功,设备初始化完成。

核心作用总结:

gpio_led_probe 函数是 leds-gpio 驱动的入口,负责:

  1. 兼容两种设备配置方式:传统平台数据(platform_data)和设备树(Device Tree)。
  2. 为每个 LED 分配资源并初始化硬件(如申请 GPIO、设置初始状态)。
  3. 将 LED 设备注册到内核 LED 子系统,生成用户空间控制接口(/sys/class/leds/)。
  4. 通过 devm_* 系列函数管理内存,确保设备卸载时资源自动释放。

这一实现体现了 Linux 驱动的兼容性设计(同时支持传统平台数据和设备树)和可靠性原则(错误回滚、自动资源管理)。

gpio_leds_create

如下:

这段代码是 leds-gpio 驱动中用于从设备树解析 LED 配置并创建 LED 设备的核心函数。当驱动通过设备树匹配设备时(而非传统 platform_data 方式),会调用该函数完成 LED 设备的初始化。

代码核心逻辑解析:

1. 函数作用

gpio_leds_create 的主要功能是:

  • 解析设备树中 LED 控制器节点的子节点(每个子节点对应一个具体 LED);
  • 从子节点中读取硬件配置(如 GPIO 引脚、默认状态、触发器等);
  • 为每个 LED 创建对应的软件结构并初始化硬件;
  • 最终返回包含所有 LED 信息的私有数据结构体。

2. 逐段解析

(1)初始化与计数子节点

struct device *dev = &pdev->dev;
struct fwnode_handle *child;
struct gpio_leds_priv *priv;
int count, ret;
struct device_node *np;
count = device_get_child_node_count(dev);  // 获取设备树中子节点数量(即 LED 数量)
if (!count)
return ERR_PTR(-ENODEV);  // 无 LED 子节点,返回错误
  • count:统计设备树中当前 LED 控制器节点下的子节点数量(每个子节点代表一个 LED)。
  • 若没有子节点,说明没有 LED 配置,返回错误。

(2)分配私有数据内存

priv = devm_kzalloc(dev, sizeof_gpio_leds_priv(count), GFP_KERNEL);
if (!priv)
return ERR_PTR(-ENOMEM);
  • 使用 devm_kzalloc 分配 gpio_leds_priv 结构体(含柔性数组 leds[]),大小由 LED 数量 count 决定。
  • 内存与设备 dev 绑定,设备销毁时自动释放。

(3)遍历设备树子节点(每个子节点对应一个 LED)

device_for_each_child_node(dev, child) {  // 遍历所有 LED 子节点
struct gpio_led led = {};  // 临时存储单个 LED 的配置
const char *state = NULL;  // 用于存储默认状态("on"/"off"/"keep")
// ... 解析子节点属性并填充 led 结构体
}
  • device_for_each_child_node:遍历设备树中当前节点的所有子节点(每个子节点描述一个 LED 的硬件信息)。

(4)解析子节点属性并初始化 LED 配置

① 获取 LED 对应的 GPIO 引脚

led.gpiod = devm_get_gpiod_from_child(dev, NULL, child);
if (IS_ERR(led.gpiod)) {
fwnode_handle_put(child);
ret = PTR_ERR(led.gpiod);
goto err;
}
  • devm_get_gpiod_from_child:从子节点中解析 gpios 属性(如 gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>),获取 GPIO 描述符 gpiod(用于后续操作 GPIO)。
  • 若获取失败,跳转至错误处理。

② 解析 LED 名称(label 属性)

if (fwnode_property_present(child, "label")) {
fwnode_property_read_string(child, "label", &led.name);  // 优先读 label 属性
} else {
if (IS_ENABLED(CONFIG_OF) && !led.name && np)
led.name = np->name;  // 无 label 则用节点名
if (!led.name)
return ERR_PTR(-EINVAL);  // 名称为空则错误
}
  • 优先从子节点的 label 属性读取 LED 名称(如 label = "user-led")。
  • 若没有 label,则使用设备树节点名作为默认名称。

③ 解析默认触发器(linux,default-trigger 属性)

fwnode_property_read_string(child, "linux,default-trigger", &led.default_trigger);
  • 读取 linux,default-trigger 属性(如 linux,default-trigger = "heartbeat"),设置 LED 的默认触发方式(如心跳模式、定时器模式等)。

④ 解析默认状态(default-state 属性)

if (!fwnode_property_read_string(child, "default-state", &state)) {
if (!strcmp(state, "keep"))
led.default_state = LEDS_GPIO_DEFSTATE_KEEP;  // 保持当前状态
else if (!strcmp(state, "on"))
led.default_state = LEDS_GPIO_DEFSTATE_ON;    // 默认点亮
else
led.default_state = LEDS_GPIO_DEFSTATE_OFF;   // 默认熄灭
}
  • 读取 default-state 属性("on"/"off"/"keep"),设置 LED 初始化时的默认状态。

⑤ 解析休眠时状态保持属性(retain-state-suspended

if (fwnode_property_present(child, "retain-state-suspended"))
led.retain_state_suspended = 1;  // 标记休眠时保持当前状态
  • 若子节点有 retain-state-suspended 属性,设置标志位,表明设备休眠时保持 LED 当前状态。

(5)创建 LED 设备并添加到私有数据

ret = create_gpio_led(&led, &priv->leds[priv->num_leds++], dev, NULL);
if (ret < 0) {
fwnode_handle_put(child);
goto err;
}
  • create_gpio_led:根据解析出的 led 配置,创建 LED 设备(注册到内核 LED 子系统,生成 /sys/class/leds/ 接口)。
  • 将创建的 LED 信息存入私有数据 priv 的柔性数组 leds[] 中,并递增计数器 num_leds

(6)错误处理

err:
for (count = priv->num_leds - 2; count >= 0; count--)
delete_gpio_led(&priv->leds[count]);  // 回滚:删除已创建的 LED
return ERR_PTR(ret);
  • 若某个 LED 创建失败,遍历已创建的 LED 并调用 delete_gpio_led 销毁,避免资源泄漏。

总结

gpio_leds_create 是设备树模式下初始化 GPIO LED 的核心函数,其工作流程可概括为:

  1. 统计设备树中 LED 子节点数量;
  2. 分配存储所有 LED 信息的私有数据内存;
  3. 逐个解析子节点的硬件属性(GPIO、名称、状态等);
  4. 为每个 LED 创建设备实例并注册到系统;
  5. 错误时回滚已创建的资源,保证系统稳定性。

该函数体现了 Linux 驱动中 “设备树解析 → 硬件配置 → 设备注册” 的标准流程,通过设备树实现了硬件信息与驱动代码的解耦。

fwnode_开头的这些函数是啥?

fwnode_ 开头的函数是 Linux 内核中用于操作固件节点(firmware node) 的通用接口,主要作用是统一处理不同类型的硬件描述信息(如设备树节点、ACPI 设备节点等),实现驱动代码对多种固件描述方式的兼容。

1. 什么是「固件节点(fwnode)」?

在 Linux 中,硬件设备的描述方式不止设备树(Device Tree)一种,还包括 ACPI(Advanced Configuration and Power Interface,主要用于 x86 平台)等。为了让驱动代码能同时支持这些不同的固件描述方式,内核抽象出了「固件节点(fwnode)」的概念:

  • 设备树中的节点(struct device_node)是一种 fwnode;
  • ACPI 中的设备节点(struct acpi_device)也是一种 fwnode;
  • fwnode_handle 是所有类型固件节点的「通用句柄」,驱动通过它可以操作任何类型的固件节点,无需关心底层是设备树还是 ACPI。

2. fwnode_* 函数的核心作用

fwnode_* 系列函数是一套通用接口,用于读取、判断固件节点的属性,无论底层是设备树还是 ACPI,驱动都可以用相同的函数获取硬件信息,从而实现「一套驱动代码兼容多种固件描述方式」。

例如:

  • 设备树用 label = "user-led" 描述 LED 名称;
  • ACPI 可能用其他方式描述同一属性;
  • 驱动通过 fwnode_property_read_string() 可以统一读取这个名称,无需区分是设备树还是 ACPI。

3. 常用 fwnode_* 函数及示例

结合你之前看到的 gpio_leds_create 函数,以下是几个高频使用的 fwnode_* 函数:

(1)fwnode_property_present()

  • 功能:判断固件节点是否包含某个属性。
  • 示例
    // 判断节点是否有 "retain-state-suspended" 属性
    if (fwnode_property_present(child, "retain-state-suspended")) {
    led.retain_state_suspended = 1;  // 有则设置标志位
    }

(2)fwnode_property_read_string()

  • 功能:从固件节点读取字符串类型的属性(如 labeldefault-state)。
  • 示例
    const char *state;
    // 读取 "default-state" 属性的值(如 "on"/"off")
    if (!fwnode_property_read_string(child, "default-state", &state)) {
    // 处理读取到的字符串
    }

(3)device_for_each_child_node()

  • 功能:遍历一个设备的所有子固件节点(类似设备树中遍历子节点)。
  • 示例
    struct fwnode_handle *child;
    // 遍历设备 dev 的所有子固件节点(如遍历所有 LED 子节点)
    device_for_each_child_node(dev, child) {
    // 处理每个子节点(如解析属性、初始化设备)
    }

(4)fwnode_handle_put()

  • 功能:释放固件节点句柄的引用(类似 kfree,但针对 fwnode)。
  • 示例
    if (IS_ERR(led.gpiod)) {
    fwnode_handle_put(child);  // 释放子节点句柄
    return PTR_ERR(led.gpiod);
    }

4. 与设备树专用函数的关系

设备树有一套专用的操作函数(如 of_property_read_stringof_get_child_count),而 fwnode_* 函数是这些函数的超集

  • 当底层是设备树时,fwnode_* 函数会自动调用对应的 of_* 函数(如 fwnode_property_read_string → of_property_read_string);
  • 当底层是 ACPI 时,fwnode_* 函数会调用 ACPI 专用函数;
  • 驱动使用 fwnode_* 函数后,无需修改代码即可在设备树和 ACPI 平台上运行。

总结

fwnode_* 函数是 Linux 内核为兼容多种固件描述方式(设备树、ACPI 等)而设计的通用接口,核心价值是统一硬件信息的读取方式,让驱动代码摆脱对特定固件类型的依赖,实现跨平台兼容。在支持设备树的驱动中,这些函数通常用于解析节点属性(如 labelgpios 等),是现代 Linux 驱动开发的标准用法。

of_node函数

在 Linux 内核设备树(Device Tree)框架中,of_node() 是一个专用的类型转换函数,核心作用是将通用固件节点句柄(struct fwnode_handle 转换为设备树特有的节点结构(struct device_node,以便访问设备树节点的专属属性和功能。

1. 先明确两个核心结构体的关系

要理解 of_node(),必须先理清其操作的两个核心结构体的层级:

结构体定位与作用
struct fwnode_handle通用固件节点句柄:内核抽象的「通用容器」,可代表任何固件类型的节点(如设备树、ACPI),不包含某类固件的专属信息。
struct device_node设备树专用节点结构:仅用于描述设备树中的节点,包含设备树特有的成员(如节点名称 name、父节点指针 parent、属性列表 properties 等)。

关键关系struct device_node 是 struct fwnode_handle 的「具体实现」—— 设备树节点在内存中存储时,会在 struct device_node 内部嵌入一个 struct fwnode_handle 成员(作为通用句柄对外暴露)。
形象理解:struct device_node 是 “完整的设备树节点”,struct fwnode_handle 是它对外提供的 “通用接口名片”,of_node() 就是通过 “名片” 找到 “完整的人”。

2. of_node() 的核心实现

of_node() 的实现非常简洁,本质是通过 container_of 宏(内核常用的结构体成员反向查找宏),从 fwnode_handle 成员的地址反推出整个 struct device_node 的地址。

其内核源码(简化版)如下:

#include   // 设备树相关头文件
static inline struct device_node *of_node(struct fwnode_handle *fwnode)
{
// 仅当 fwnode 是设备树节点的通用句柄时,转换才有效
if (!fwnode || fwnode->type != FWNODE_OF)
return NULL;
// 通过 fwnode_handle 成员,反向获取包含它的 device_node 结构体地址
return container_of(fwnode, struct device_node, fwnode);
}
  • container_of(ptr, type, member):内核宏,作用是 “已知结构体 type 中的成员 member 的地址 ptr,反推出整个 type 结构体的首地址”。
  • fwnode->type != FWNODE_OF:安全检查,确保传入的 fwnode 确实是设备树类型的节点(避免对 ACPI 等其他固件节点误转换)。

3. of_node() 的使用场景

of_node() 仅在需要从通用固件节点访问设备树专属功能时使用,最典型的场景是:当驱动代码通过通用的 fwnode_* 接口拿到节点句柄后,需要进一步获取设备树节点的特有信息(如节点名称、父节点等)。

结合你之前提供的代码示例,就能清晰看到其用途:

// 1. 从通用固件节点句柄(child,类型为 fwnode_handle*)转换为设备树节点(np,类型为 device_node*)
struct device_node *np = of_node(child);
// 2. 访问设备树节点的专属成员:np->name(设备树节点的名称,如 "led0"、"uart1")
if (IS_ENABLED(CONFIG_OF) && !led.name && np)
led.name = np->name;  // 只有 device_node 才有 name 成员,fwnode_handle 没有
  • 若直接用 fwnode_handle,无法获取 name(通用句柄不包含该字段);
  • 必须通过 of_node() 转换为 device_node,才能访问设备树节点的原生名称。

4. 使用注意事项

(1)必须配合设备树启用配置

of_node() 依赖内核的设备树支持,因此使用前通常需要用 IS_ENABLED(CONFIG_OF) 做编译期检查,避免在未启用设备树的内核中报错:

if (IS_ENABLED(CONFIG_OF) && np) {  // 确保设备树功能已启用
// 访问 np 的设备树专属成员
}

(2)输入句柄必须是设备树类型

若传入的 fwnode_handle 是 ACPI 或其他固件类型的节点(fwnode->type != FWNODE_OF),of_node() 会返回 NULL,直接使用会导致空指针错误。因此需先判断返回值是否有效:

struct device_node *np = of_node(child);
if (!np) {
dev_err(dev, "fwnode is not a device tree node\n");
return -EINVAL;
}

(3)与 fwnode_* 函数的配合

of_node() 与通用的 fwnode_* 函数(如 fwnode_property_read_string)是互补关系,而非替代:

  • 优先用 fwnode_* 函数:处理属性读取(如读 labeldefault-state)等通用操作,确保驱动兼容设备树、ACPI 等多种固件;
  • 必要时用 of_node():当需要设备树特有功能(如读节点名 np->name、用 of_property_match_string 等 of_* 专用 API)时,再转换为 device_node

create_gpio_led

create_gpio_led 函数是 leds-gpio 驱动的核心实现,负责将一个 GPIO 控制的 LED 设备注册到内核 LED 子系统,完成从硬件配置到用户空间控制接口的完整初始化。其核心功能是将 GPIO 引脚与 LED 设备逻辑绑定,并向系统注册可操作的 LED 设备

代码核心逻辑解析:

1. 函数作用

create_gpio_led 主要完成三件事:

  • 申请并初始化 LED 对应的 GPIO 资源(兼容传统 GPIO 编号和现代 gpio_desc 描述符);
  • 配置 LED 设备的基本属性(名称、默认亮度、触发器、控制函数等);
  • 将 LED 设备注册到内核 LED 子系统,生成用户空间控制接口(/sys/class/leds/<name>/)。

2. 逐段解析

(1)初始化 GPIO 描述符(优先使用现代 gpio_desc

led_dat->gpiod = template->gpiod;  // 从模板获取 gpio_desc(现代方式)
if (!led_dat->gpiod) {
// 若没有 gpio_desc,则走传统 GPIO 编号的兼容路径
unsigned long flags = 0;
// 检查 GPIO 编号是否有效
if (!gpio_is_valid(template->gpio)) {
dev_info(parent, "Skipping unavailable LED gpio %d (%s)\n",
template->gpio, template->name);
return 0;
}
// 处理低电平有效(active_low)标志
if (template->active_low)
flags |= GPIOF_ACTIVE_LOW;
// 申请 GPIO 资源(自动释放)
ret = devm_gpio_request_one(parent, template->gpio, flags,
template->name);
if (ret gpiod = gpio_to_desc(template->gpio);
if (IS_ERR(led_dat->gpiod))
return PTR_ERR(led_dat->gpiod);
}
  • 现代方式:优先使用 template->gpiodstruct gpio_desc*,GPIO 描述符,设备树或现代驱动常用)。
  • 兼容传统方式:若没有 gpiod,则使用传统 GPIO 编号(template->gpio),通过 devm_gpio_request_one 申请 GPIO 资源,并转换为 gpiod(统一后续操作接口)。
  • 两种方式最终都确保 led_dat->gpiod 有效(指向该 LED 对应的 GPIO 描述符)。

(2)配置 LED 核心设备属性

// 配置 LED 设备名称(用户空间可见,如 /sys/class/leds/user-led/)
led_dat->cdev.name = template->name;
// 配置默认触发器(如 "heartbeat" 心跳模式)
led_dat->cdev.default_trigger = template->default_trigger;
// 标记 GPIO 是否可能休眠(如使用 I2C GPIO 扩展器时)
led_dat->can_sleep = gpiod_cansleep(led_dat->gpiod);
led_dat->blinking = 0;  // 初始化为非闪烁状态
// 配置闪烁控制函数(若平台提供)
if (blink_set) {
led_dat->platform_gpio_blink_set = blink_set;  // 平台特定闪烁函数
led_dat->cdev.blink_set = gpio_blink_set;      // 通用闪烁接口
}
// 配置亮度控制函数(核心控制逻辑)
led_dat->cdev.brightness_set = gpio_led_set;
  • led_dat->cdev 是 struct led_classdev 类型,是内核 LED 子系统的标准设备结构,用于向系统注册 LED 设备。
  • 通过配置 brightness_set 和 blink_set 函数,定义了用户空间控制 LED 亮度和闪烁的底层实现。

(3)设置 LED 初始状态

// 确定初始亮度状态
if (template->default_state == LEDS_GPIO_DEFSTATE_KEEP) {
// 保持当前 GPIO 状态(读取硬件当前值)
state = !!gpiod_get_value_cansleep(led_dat->gpiod);
} else {
// 根据默认状态(ON/OFF)设置
state = (template->default_state == LEDS_GPIO_DEFSTATE_ON);
}
// 初始化亮度值(LED_FULL 表示最大亮度,LED_OFF 表示熄灭)
led_dat->cdev.brightness = state ? LED_FULL : LED_OFF;
// 若休眠时不需要保持状态,则设置 suspend/resume 标志
if (!template->retain_state_suspended)
led_dat->cdev.flags |= LED_CORE_SUSPENDRESUME;
  • 初始状态由 template->default_state 决定(来自设备树 default-state 属性或 platform_data)。
  • gpiod_get_value_cansleep:安全读取 GPIO 当前值(支持可能休眠的 GPIO 设备)。

(4)配置 GPIO 方向并注册 LED 设备

// 将 GPIO 配置为输出模式,并设置初始电平(state 为 0 或 1)
ret = gpiod_direction_output(led_dat->gpiod, state);
if (ret work, gpio_led_work);
// 将 LED 设备注册到内核 LED 子系统
return led_classdev_register(parent, &led_dat->cdev);
  • gpiod_direction_output:将 GPIO 设为输出,并设置初始电平(与 state 一致)。
  • led_classdev_register:核心注册函数,完成后会在 /sys/class/leds/ 下生成以 template->name 命名的目录,包含 brightnesstrigger 等控制文件,用户可通过这些文件操作 LED(如 echo 1 > brightness 点亮)。

总结

create_gpio_led 是 GPIO LED 设备的 “初始化引擎”,其工作流程可概括为:

  1. 兼容处理 GPIO 资源(现代 gpio_desc 或传统编号);
  2. 配置 LED 设备的核心属性(名称、触发器、控制函数);
  3. 设置初始状态并将 GPIO 设为输出;
  4. 注册设备到内核 LED 子系统,开放用户空间接口。

通过这个函数,硬件层面的 GPIO 引脚被抽象为软件层面的 LED 设备,实现了 “硬件操作” 与 “用户控制” 的解耦,是 Linux 驱动中 “硬件抽象” 思想的典型体现。

巩固:

Linux设备驱动里,是先初始化再注册到内核,还是先注册再初始化?

在 Linux 设备驱动中,标准流程是 “先初始化,再注册”—— 即先完成设备 / 驱动的所有初始化工作(如资源申请、参数配置、状态设置等),再调用内核提供的注册接口将其注册到内核子系统中。这样可以确保内核管理的是一个 “就绪状态” 的设备 / 驱动,避免注册后因未初始化完全而导致的异常。

具体原因与示例:

1. 驱动注册的本质

内核的 “注册” 操作本质是将设备 / 驱动的信息录入内核管理框架(如平台驱动框架、字符设备框架、LED 子系统等),并使其具备被内核调度或被用户空间访问的能力。
如果先注册后初始化,内核可能在初始化完成前就尝试访问设备(如调用操作函数),导致未定义行为(如空指针、资源未就绪)。

2. 典型驱动的 “初始化→注册” 流程

以你熟悉的 leds-gpio 驱动为例,create_gpio_led 函数完全遵循这一流程:

  • 先初始化:完成了 GPIO 资源申请、设备名称设置、操作函数绑定、硬件初始状态配置等所有必要工作。
  • 后注册:调用 led_classdev_register 将设备加入内核 LED 子系统,此时内核可以安全地通过注册的操作函数(如 brightness_set)控制设备。

3. 其他驱动类型的相同逻辑

无论是平台驱动、字符设备还是网络设备,均遵循这一原则:

  • 平台驱动

    static int my_probe(...) {
    // 初始化:申请内存、解析设备树、配置硬件
    priv = devm_kzalloc(...);
    parse_dt(priv);
    init_hw(priv);
    // 注册:将设备注册到相应子系统(如字符设备)
    cdev_add(&priv->cdev, devt, 1);
    }
  • 字符设备

    static int char_dev_init(void) {
    // 初始化:设置file_operations、分配设备号
    cdev_init(&my_cdev, &fops);
    alloc_chrdev_region(&devt, 0, 1, "mydev");
    // 注册:添加到内核字符设备表
    cdev_add(&my_cdev, devt, 1);
    }

4. 反例:先注册后初始化的风险

如果颠倒顺序(先注册后初始化),可能导致:

  • 内核在初始化完成前调用设备的操作函数(如用户空间读写设备),访问未初始化的资源(如空指针)。
  • 硬件状态未就绪(如 GPIO 未配置方向),导致操作失败或硬件异常。

总结

Linux 设备驱动的标准流程是 **“先初始化,后注册”**:

  • 初始化:完成资源申请、参数配置、硬件就绪等所有必要准备工作。
  • 注册:将设备 / 驱动信息提交给内核管理框架,使其正式 “上线”。

这一流程确保了内核管理的设备处于可用状态,是驱动可靠性的基础,也是 Linux 驱动开发的通用规范。

http://www.wxhsa.cn/company.asp?id=5281

相关文章:

  • DP 凸性优化:wqs 二分
  • 浦东再添一所一流高校,上海交通大学医学院浦东校区正式启用
  • nccl study
  • AI服务器公开招标大面积失败,中国联通“招”了个寂寞?
  • 【GitHub每日速递 250916】2053 个 n8n 工作流曝光!365 种集成 + 可视化管理,效率直接拉满
  • 每日一家公司职场内幕——龙旗科技(上海)
  • 0129_迭代器模式(Iterator)
  • HJ7 取近似值
  • 读人形机器人13艺术领域
  • 活动报名:Voice First!Demo Day@Voice Agent Camp,9.22,上海丨超音速计划 2025
  • Windows计算器:现代C++实现的多功能计算工具
  • 使用 PySide6/PyQt6 实现系统图标的展示与交互
  • 如何让Java的线程池顺序执行任务 ?
  • Git 提交排除文件夹方法总结
  • 如何在 Ubuntu24.04 TLS 上安装 Kubernetes 集群 - Antonie
  • Jmeter的插件开发
  • Educational Codeforces Round 182 (Rated for Div. 2)
  • java第二周课前提问
  • java GC
  • Redis最佳实践——性能优化技巧之监控与告警详解
  • week1
  • EF Core 与 MySQL:迁移和关系配置详解
  • 《原子习惯》-读书笔记2
  • CF1626D 题解
  • Python 集合运算:并集、交集、差集全解析
  • 第一周数据可视化作业
  • 用 C++ + OpenCV + Tesseract 实现英文数字验证码识别
  • java 第一节课课前提问
  • 二进制解码器、选通器和分配器
  • 2025最新版 Photoshop软件免费下载安装完整教程(PS2025)超详细安装教程