[TOC]

在这里插入图片描述

drivers/leds LED子系统(LED Subsystem) 内核统一的LED设备控制框架

历史与背景

这项技术是为了解决什么特定问题而诞生的?

LED子系统的诞生是为了解决在Linux内核中缺乏一个统一、抽象的LED管理方式的问题。在此框架出现之前,对LED的控制是零散且混乱的:

  • 驱动代码重复:每个需要控制LED的驱动程序(例如,网卡驱动、SD卡驱动)都必须自己去实现操作GPIO或特定硬件寄存器的代码,导致大量功能相似的代码被重复编写。
  • 缺乏统一的用户接口:用户无法以一种标准的方式来查看系统中有哪些LED,也无法在运行时改变它们的行为。控制LED的逻辑被硬编码在各自的驱动中。
  • 逻辑与硬件强耦合:LED的“物理”控制(如何点亮它)和“逻辑”行为(为什么点亮它,例如因为网络活动)被混杂在一起。这使得更换LED硬件或改变其用途变得非常困难。

LED子系统的核心目标就是创建一个清晰的框架,将LED的物理控制触发其亮灭的系统事件彻底解耦,并为用户空间提供一个标准的控制接口。

它的发展经历了哪些重要的里程碑或版本迭代?

LED子系统的发展核心是触发器(Trigger)机制的引入和完善,这是其设计的精髓所在。

  • 基础框架建立:最初,子系统定义了led_classdev结构体,为所有LED设备提供了一个通用的注册接口和基于sysfs的属性(如brightness)。这解决了驱动代码重复和缺乏统一接口的问题。
  • 触发器机制的引入:这是最重要的里程碑。内核引入了led_trigger的概念,它代表一种可以触发LED状态变化的系统事件。例如,“硬盘活动”是一个触发器,“网络活动”是另一个。任何LED设备都可以通过sysfs在运行时动态地“挂接”到任何一个可用的触发器上。这完美地实现了逻辑与硬件的解耦。
  • 常见触发器的标准化:随着框架的成熟,社区逐步添加了许多通用的、可复用的触发器,如heartbeat(心跳,表示系统正常运行)、timer(定时闪烁)、disk-activity(磁盘活动)、netdev(网络设备活动)、input(响应输入事件,如大小写锁定键)等。
  • 支持更复杂的LED:框架从最初只支持简单的亮/灭,扩展到支持多级亮度控制、RGB颜色控制等更复杂的LED硬件。

目前该技术的社区活跃度和主流应用情况如何?

LED子系统是Linux内核中一个非常稳定、成熟且被广泛应用的基础设施。它在各种类型的Linux设备上都扮演着重要的角色。

  • 主流应用
    • 嵌入式设备和路由器:用于显示电源、网络状态、Wi-Fi信号强度等。
    • 服务器和NAS:用于显示硬盘托架的活动或故障状态。
    • 单板计算机(如树莓派):用于显示电源和SD卡活动状态。
    • 笔记本电脑:用于控制电源、Wi-Fi、大小写锁定等状态指示灯。

核心原理与设计

它的核心工作原理是什么?

LED子系统的核心是设备驱动触发器分离的生产者/消费者模型。

  1. LED设备驱动(生产者)
    • 这是一个知道如何控制一个物理LED的驱动程序。例如,leds-gpio驱动知道如何通过翻转GPIO电平来点亮/熄灭LED;leds-pca9532驱动知道如何通过I2C总线与一个LED控制器芯片通信。
    • 它会填充并注册一个struct led_classdev实例。这个结构体中最核心的回调函数是.brightness_set(),LED核心层通过调用它来改变LED的亮度(0代表熄灭)。
  2. LED核心层(协调者)
    • 这是位于drivers/leds/led-class.c等文件中的子系统核心。
    • 它管理所有注册的led_classdev,并在sysfs中为它们创建目录(通常在/sys/class/leds/下)。
    • 它还管理所有注册的led_trigger,并通过sysfs文件trigger让用户可以进行关联。
  3. LED触发器(消费者/事件源)
    • 这是一个代表系统事件的模块。例如,ledtrig-disk.c会在块设备层有I/O活动时被通知。
    • 当一个触发器被关联到一个LED设备上,并且其代表的事件发生时,触发器会调用LED核心层的函数(如led_trigger_event())。
    • 最终,LED核心层会调用具体LED设备驱动的.brightness_set()回调,完成对物理LED的控制。
  4. 用户空间接口(Sysfs)
    • 用户可以通过/sys/class/leds/下的目录来与LED交互。
    • cat brightness可以查看当前亮度。
    • echo 255 > brightness可以手动设置亮度。
    • cat trigger可以查看当前关联的触发器和所有可用的触发器(当前触发器会被方括号[]括起来)。
    • echo disk-activity > trigger可以将这个LED的行为模式切换为“硬盘活动指示灯”。

它的主要优势体现在哪些方面?

  • 彻底解耦:将“如何亮”和“为何亮”分开,极大地提高了代码的模块化和复用性。
  • 用户可定制:允许用户在系统运行时,通过简单的shell命令动态改变任何LED的功能,提供了极大的灵活性。
  • 代码复用:触发器(如heartbeat)和设备驱动(如leds-gpio)都可以被无限次复用。
  • 标准化:为所有LED提供了一致的接口和行为模式。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 不适用于复杂灯效:该框架主要为简单的状态指示而设计。对于需要高速、同步、复杂动画效果的场景(如RGB电竞键盘/鼠标),它的sysfs接口性能不足,且缺乏复杂的模式编程能力。
  • 非显示设备:它不适用于需要显示复杂信息(如字符、数字)的设备,例如7段数码管。
  • 与Backlight子系统的界限:虽然LED子系统可以控制亮度,但对于屏幕或键盘背光这类用于“照明”而非“指示”的设备,有专门的Backlight子系统,它与电源管理和图形栈的集成更好。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

LED子系统是任何需要在硬件上提供系统状态指示的标准解决方案。

  • 网络设备活动:将一个以太网口的LED关联到netdev触发器。可以配置它在有网络流量(TX/RX)、或链接状态(Link Up)时闪烁/点亮。
  • 系统“心跳”:在一个没有屏幕的嵌入式设备上,将一个LED关联到heartbeat触发器,使其有规律地闪烁。这可以直观地判断系统是否仍在正常运行,还是已经死机。
  • 存储活动:将服务器硬盘托架上的LED关联到disk-activity或具体的scsi_host触发器,以显示哪个硬盘正在进行读写操作。
  • 按键状态:笔记本上的大小写锁定(Caps Lock)灯。input子系统会生成事件,ledtrig-input触发器捕获这个事件并点亮/熄灭由leds-gpio控制的Caps Lock LED。

是否有不推荐使用该技术的场景?为什么?

  • RGB电竞外设:不推荐。这些设备通常通过USB HID协议与用户空间的守护进程通信,以实现复杂的、可编程的灯光效果。
  • LCD/OLED屏幕背光:不推荐。应使用Backlight子系统,因为它提供了更符合语义的接口,并且能更好地与系统的显示和电源管理策略(如空闲时自动调暗)集成。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 LED Subsystem Backlight Subsystem 直接GPIO控制 (无框架)
核心功能 状态指示 (Indication)。核心是“为什么亮”(触发器)。 照明 (Illumination)。核心是“有多亮”(亮度级别)。 直接的物理控制
抽象层次 。将物理LED与逻辑事件完全分离。 中等。抽象了不同背光硬件的亮度控制,但没有触发器概念。 无抽象。驱动直接与硬件打交道。
用户接口 Sysfs (/sys/class/leds),提供亮度、触发器等丰富控制。 Sysfs (/sys/class/backlight),主要提供亮度和电源状态控制。 无标准用户接口。
典型设备 状态指示灯(电源、网络、磁盘活动、Caps Lock)。 LCD屏幕背光、键盘背光。 任何需要简单开关控制的场景,但只在没有更好框架时才应考虑。
灵活性 非常高。用户可在运行时任意改变LED的功能。 中等。用户可以调节亮度,但其功能是固定的。 极低。功能在驱动中硬编码,无法更改。
适用场景 需要向用户传达系统状态信息 需要为用户提供视觉照明 在极简的、不需要任何抽象或用户控制的场景下(现代内核中几乎不存在)。

drivers/leds/led-core.c

LED 亮度更新与设置

此代码片段包含了Linux LED子系统中两个核心的API函数, 用于获取和设置LED的亮度。它们是LED驱动程序、触发器以及sysfs接口之间交互的中枢。其核心原理是提供一个统一的、健壮的接口来改变LED状态, 同时优雅地处理与软件控制的”闪烁”(blinking)功能之间的复杂交互


led_update_brightness: 同步软件状态与硬件现实

这个函数提供了一个”拉”(pull)机制, 用于查询LED硬件的当前实际亮度, 并更新内核中对应的软件状态。

  • 原理: 某些LED硬件或控制器可能具有自主改变亮度的能力(例如, 硬件控制的”呼吸”效果), 或者其状态可能被其他方式改变。这个函数允许LED子系统在需要时(例如, 当用户通过sysfs读取亮度时)向底层硬件驱动查询最新的状态。这是一个可选功能, 只有当硬件驱动在led_classdev结构体中提供了brightness_get回调函数时, 此机制才会生效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
* led_update_brightness: 更新LED的亮度状态.
* @led_cdev: 目标LED设备.
* @return: 成功时返回0, 失败时返回错误码.
*/
int led_update_brightness(struct led_classdev *led_cdev)
{
int ret;

/*
* 检查底层硬件驱动是否提供了 .brightness_get() 回调函数.
* 这是一个可选的接口, 只有支持读取当前亮度的硬件才需要实现它.
*/
if (led_cdev->brightness_get) {
/*
* 如果提供了, 就调用它来从硬件获取当前的亮度值.
*/
ret = led_cdev->brightness_get(led_cdev);
if (ret < 0)
return ret; /* 如果读取失败, 则向上层传递错误. */

/*
* 将内核中缓存的亮度值 led_cdev->brightness 更新为从硬件读取到的最新值.
* 这确保了软件状态与硬件状态的同步.
*/
led_cdev->brightness = ret;
}

/*
* 如果驱动没有提供 .brightness_get, 或者操作成功, 都返回0.
*/
return 0;
}
/* 导出此符号, 供其他内核模块(主要是 sysfs 回调)使用. */
EXPORT_SYMBOL_GPL(led_update_brightness);

LED 亮度设置核心逻辑

此代码片段揭示了Linux LED子系统中负责设置亮度的核心”引擎”。它由两个函数组成, 共同实现了一个健壮、高效且支持异步操作的亮度设置机制。其核心原理是采用”快速路径/慢速路径”(Fast Path/Slow Path)的设计模式: 优先尝试一个快速、非阻塞的操作; 如果该操作不可行(因为它可能需要睡眠), 则将任务安全地委托给一个内核工作队列(workqueue)进行异步处理


led_set_brightness_nosleep: 安全的亮度设置入口点

这个函数是led_set_brightness的下一层实现, 充当了一个安全的”预处理器”和入口点。它负责在尝试改变硬件状态之前, 对输入值进行清理并检查设备的电源状态。

  • 原理:
    1. 输入值净化: 它使用min()函数确保请求的亮度值不会超过硬件所能支持的最大亮度。这是一个重要的健壮性措施, 防止向底层驱动传递无效值。
    2. 电源状态检查: 它会检查LED_SUSPENDED标志, 如果设备当前处于挂起(suspend)状态, 它会更新软件中缓存的亮度值, 但会跳过所有硬件操作。这确保了在设备恢复(resume)后, 可以设置正确的亮度, 同时避免了在设备电源关闭时访问硬件寄存器。
    3. 分发: 在通过所有检查后, 它调用led_set_brightness_nopm来执行真正的设置逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
* led_set_brightness_nosleep: 设置一个LED的亮度 (此函数本身不会睡眠).
* @led_cdev: 目标LED设备.
* @value: 要设置的新亮度值.
*/
void led_set_brightness_nosleep(struct led_cdev *led_cdev, unsigned int value)
{
/*
* 将请求的亮度值 'value' 与硬件支持的最大亮度 'max_brightness' 比较,
* 取两者中的较小值. 这可以防止设置一个超出硬件能力的无效亮度.
* 结果被保存在软件状态缓存中.
*/
led_cdev->brightness = min(value, led_cdev->max_brightness);

/*
* 检查LED设备是否处于挂起状态.
*/
if (led_cdev->flags & LED_SUSPENDED)
return; /* 如果已挂起, 则只更新软件状态, 不进行任何硬件操作, 直接返回. */

/*
* 调用下一层函数, 使用经过净化的亮度值来执行实际的设置操作.
*/
led_set_brightness_nopm(led_cdev, led_cdev->brightness);
}
/* 导出此符号, 供其他内核模块使用. */
EXPORT_SYMBOL_GPL(led_set_brightness_nosleep);

led_set_brightness_nopm: 快速路径/慢速路径分发器

这个函数是亮度设置的核心决策者。它决定是立即完成操作, 还是将其推迟到工作队列中。

  • 原理:
    1. 尝试快速路径: 它首先调用__led_set_brightness。这个内部函数通常会调用硬件驱动提供的brightness_set_blocking回调, 该回调被保证不会睡眠。如果硬件操作非常简单(例如, 写入一个内存映射的寄存器), 这个回调就会存在并成功返回0。此时, 任务完成, 函数直接返回。
    2. 切换到慢速路径: 如果__led_set_brightness失败(返回非0), 这意味着硬件驱动的亮度设置操作可能会睡眠(例如, 通过I2C或SPI总线通信)。由于当前上下文可能不允许睡眠(例如, 在中断处理程序或持有自旋锁时), 必须采用异步方式。
    3. 准备工作: 它将要设置的亮度值存入delayed_set_value
    4. 设置信标: 它使用原子操作(set_bit/clear_bit)来设置work_flags中的标志位。这些标志就像是给即将运行的工作队列任务的”指令”。它为”设置为非零值”和”设置为零(关闭)”这两种情况设置了不同的标志, 因为关闭LED可能需要额外处理(如停止硬件闪烁)。
    5. 调度任务: 最后, 它调用queue_workset_brightness_work任务放入leds_wq工作队列中。内核的工作队列线程会在稍后一个安全的、允许睡眠的上下文中执行这个任务, 任务会读取work_flagsdelayed_set_value来完成实际的硬件操作。

(单核)系统上, smp_mb__before_atomic()主要作为编译器屏障, 防止编译器对内存访问和原子操作进行不安全的重排序, 这对于保证在抢占式内核中的逻辑正确性仍然是必要的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
static int __led_set_brightness(struct led_classdev *led_cdev, unsigned int value)
{
if (!led_cdev->brightness_set)
return -ENOTSUPP;

led_cdev->brightness_set(led_cdev, value);

return 0;
}

/*
* led_set_brightness_nopm: 设置亮度, 不直接涉及电源管理(Power Management).
* @led_cdev: 目标LED设备.
* @value: 要设置的新亮度值.
*/
void led_set_brightness_nopm(struct led_cdev *led_cdev, unsigned int value)
{
/* --- 快速路径 --- */
/*
* 尝试调用 __led_set_brightness, 它会使用驱动提供的非睡眠接口.
* 如果返回0 (成功), 说明亮度已设置, 直接返回.
*/
if (!__led_set_brightness(led_cdev, value))
return;

/* --- 慢速路径 (需要工作队列) --- */
/*
* 亮度设置操作可能会睡眠, 将其委托给工作队列任务.
* 将要设置的值存入一个临时变量, 供工作队列任务读取.
*/
led_cdev->delayed_set_value = value;
/*
* 内存屏障: 确保对 delayed_set_value 的写入, 在多核系统上,
* 对所有CPU都可见, 之后才能执行后续的原子操作.
* 在单核系统上, 它主要用作编译器屏障, 防止指令重排.
*/
smp_mb__before_atomic();

if (value) {
/* 如果要设置的亮度值非0, 设置 'SET_BRIGHTNESS' 标志. */
set_bit(LED_SET_BRIGHTNESS, &led_cdev->work_flags);
} else {
/*
* 如果要关闭LED (value为0), 这是一个特殊情况.
* 它可能还需要禁用硬件闪烁. 为了防止这个"关闭"操作
* 被紧随其后的另一个亮度修改请求丢失, 它使用了一个独立的标志.
*/
clear_bit(LED_SET_BRIGHTNESS, &led_cdev->work_flags);
clear_bit(LED_SET_BLINK, &led_cdev->work_flags);
set_bit(LED_SET_BRIGHTNESS_OFF, &led_cdev->work_flags);
}

/*
* 将 set_brightness_work 任务放入工作队列中, 内核线程稍后会执行它.
*/
queue_work(led_cdev->wq, &led_cdev->set_brightness_work);
}
/* 导出此符号, 供其他内核模块使用. */
EXPORT_SYMBOL_GPL(led_set_brightness_nopm);

led_set_brightness: 设置LED亮度的主要API

这个函数是设置LED亮度的标准入口点。它的设计非常巧妙, 充当了一个”分发器”(dispatcher), 能够正确处理普通设置和软件闪烁这两种截然不同的情况。

  • 原理: 软件闪烁是通过一个定时器和工作队列(workqueue)实现的, 它会周期性地改变LED的亮度。如果此时有一个新的亮度设置请求进来, 就会产生竞争条件: 新设置的亮度可能在下一个定时器滴答(tick)时被闪烁逻辑覆盖掉。此函数通过以下方式解决这个问题:

    1. 检查闪烁状态: 它首先使用test_bit(LED_BLINK_SW, ...)原子地检查软件闪烁功能是否处于激活状态。
    2. 与闪烁逻辑协作: 如果正在闪烁:
      • 请求关闭闪烁: 如果新亮度为0, 它不会直接关闭LED, 而是设置一个LED_BLINK_DISABLE标志, 然后调度工作队列任务。这相当于给正在运行的闪烁任务发送一个”请自行优雅关闭”的信号。这样做可以避免在中断等不允许睡眠的上下文中直接操作定时器。
      • 请求改变闪烁亮度: 如果新亮度非0, 它也不会直接设置, 而是设置一个LED_BLINK_BRIGHTNESS_CHANGE标志, 并将新亮度值存入new_blink_brightness。闪烁任务在下一次运行时会检查这个标志, 并自动采用新的亮度值作为其”亮”状态的电平。
    3. 直接设置: 如果当前没有在进行软件闪烁, 函数会直接调用led_set_brightness_nosleep, 这将最终调用底层硬件驱动的.brightness_set()回调函数来实际改变硬件状态。
  • 工作队列: queue_work用于将可能引起睡眠的操作(如修改定时器)从中断上下文(Interrupt Context)推迟到进程上下文(Process Context)中执行, 这是编写健壮驱动程序的关键。

  • 原子操作: test_bitset_bit是原子操作, 它们可以防止在检查和设置work_flags的过程中被中断或任务抢占, 从而避免了在单核抢占式系统上的竞态条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
* led_set_brightness: 设置一个LED的亮度.
* @led_cdev: 目标LED设备.
* @brightness: 要设置的新亮度值.
*/
void led_set_brightness(struct led_classdev *led_cdev, unsigned int brightness)
{
/*
* 检查软件闪烁标志位. test_bit 是一个原子操作.
*/
if (test_bit(LED_BLINK_SW, &led_cdev->work_flags)) {
/*
* 如果我们想在闪烁时关闭LED (设置亮度为0),
* 我们需要将这个任务委托给工作队列, 以避免在中断上下文中
* 调用可能睡眠的函数(如del_timer_sync).
*/
if (!brightness) {
/*
* 设置一个标志, 告诉工作队列任务: "请停止闪烁".
*/
set_bit(LED_BLINK_DISABLE, &led_cdev->work_flags);
/*
* 调度工作队列中的 set_brightness_work 任务来执行清理工作.
*/
queue_work(led_cdev->wq, &led_cdev->set_brightness_work);
} else {
/*
* 如果我们想在闪烁时改变亮度, 我们只需更新目标亮度值,
* 并设置一个标志. 闪烁任务会在下一次运行时采用这个新值.
*/
set_bit(LED_BLINK_BRIGHTNESS_CHANGE,
&led_cdev->work_flags);
led_cdev->new_blink_brightness = brightness;
}
return; /* 任务已委托, 直接返回. */
}

/*
* 如果没有在进行软件闪烁, 就直接调用底层函数来设置亮度.
* _nosleep 版本暗示它可能被设计为可以在原子上下文中调用.
*/
led_set_brightness_nosleep(led_cdev, brightness);
}
/* 导出此符号, 供其他内核模块(触发器、驱动等)使用. */
EXPORT_SYMBOL_GPL(led_set_brightness);

此函数是一个核心辅助API, 其唯一的作用是安全、完整地停止由内核定时器驱动的LED软件闪爍功能。当一个触发器被移除, 或者用户通过sysfs写入亮度0来关闭闪烁时, 就会调用此函数。

它的工作原理是执行一个严格的、有序的清理流程, 以确保将LED的状态从”定时器驱动”模式彻底转换回”手动控制”模式, 同时避免任何竞态条件。

  1. 同步删除定时器: timer_delete_sync(&led_cdev->blink_timer)是此函数最关键的操作。blink_timer是周期性触发LED亮灭状态改变的内核定时器。timer_delete_sync函数会停用这个定时器。_sync后缀至关重要, 它保证了函数会等待定时器的处理函数(handler)执行完毕后才返回(如果它恰好正在运行)。这可以防止在处理函数仍在访问led_cdev数据时, 其他代码已经开始修改这些数据, 从而杜绝了”use-after-free”或数据竞争的风险。
  2. 状态复位: 在确认定时器已完全停止后, 函数将与闪烁相关的状态变量blink_delay_onblink_delay_off清零。这是一种良好的编程实践, 可以防止在未来重新启用闪烁时, 意外地使用了过时的、无效的延迟值。
  3. 清除标志位: clear_bit(LED_BLINK_SW, &led_cdev->work_flags)以原子操作的方式清除LED_BLINK_SW状态标志。这个标志是其他函数(如led_set_brightness)用来判断是否应将亮度设置请求委托给闪烁逻辑的依据。清除此标志后, LED的行为会立即恢复到直接响应亮度设置请求的模式。
  • timer_delete_sync的同步特性依然重要, 它可以防止在定时器中断刚发生、其对应的软中断(softirq)即将执行时, 出现状态不一致的问题。
  • clear_bit的原子性保证了即使在读-修改-写work_flags的过程中发生任务抢占或中断, 也不会破坏该变量的完整性, 这对于编写健壮的内核代码至关重要。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*
* led_stop_software_blink: 停止一个LED的软件控制闪烁.
* @led_cdev: 目标LED设备.
*/
void led_stop_software_blink(struct led_classdev *led_cdev)
{
/*
* 调用 timer_delete_sync() 来安全地删除和停用闪烁定时器.
* '_sync' 后缀保证了此函数会等待定时器的回调函数执行完毕(如果它正在运行).
* 这可以防止在定时器回调仍在运行时, 其他代码修改了 led_cdev 的状态, 从而避免了竞态条件.
* 这是停止闪烁最关键的一步.
*/
timer_delete_sync(&led_cdev->blink_timer);
/*
* 将闪烁的 "亮" 时间周期清零.
*/
led_cdev->blink_delay_on = 0;
/*
* 将闪烁的 "灭" 时间周期清零.
* 这两步操作将闪烁的状态彻底清除, 是一种良好的清理实践.
*/
led_cdev->blink_delay_off = 0;
/*
* 调用 clear_bit() 以原子操作的方式清除软件闪烁(LED_BLINK_SW)的状态标志.
* "原子操作"确保了即使在单核抢占式内核中, 这个读-修改-写的过程也不会被中断.
* 清除此标志后, led_set_brightness() 等函数将不再把请求委托给闪烁逻辑,
* 而是会直接设置LED的亮度.
*/
clear_bit(LED_BLINK_SW, &led_cdev->work_flags);
}
/*
* 导出此符号, 使其可被其他内核模块(主要是触发器驱动)调用.
*/
EXPORT_SYMBOL_GPL(led_stop_software_blink);

drivers/leds/led-triggers.c

LED sysfs 触发器(Trigger)读取实现

此代码片段实现了用户通过sysfs文件系统读取一个LED可用”触发器”列表的功能, 对应于cat /sys/class/leds/.../trigger命令。其核心原理是动态地生成一个格式化的字符串, 该字符串列出了所有已注册且与当前LED相关的触发器, 并用方括号[]明确标识出当前正被激活的触发器

由于在某些极端情况下(如拥有数千个CPU核心的系统), “cpu”触发器的数量可能非常多, 导致列表的长度超过sysfs普通文本属性4KB的页面大小限制, 因此这里采用了**二进制属性(bin_attribute)**的实现方式。这是一种更灵活但不推荐常规使用的sysfs接口, 它允许读写任意长度的数据。


led_trigger_read: trigger文件的读操作回调

这是当用户空间读取trigger文件时, 内核调用的主函数。它采用了一种健壮且高效的”两遍式”(two-pass)策略来生成和返回数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/*
* (注释节选) ...我们在这里通过创建二进制属性来解决它, 它不受长度限制.
* 这_不是_好的设计, 不要模仿它.
*/
ssize_t led_trigger_read(struct file *filp, struct kobject *kobj,
const struct bin_attribute *attr, char *buf,
loff_t pos, size_t count)
{
/* 从 kobject 获取 device, 再从 device 获取 led_classdev 私有数据 */
struct device *dev = kobj_to_dev(kobj);
struct led_classdev *led_cdev = dev_get_drvdata(dev);
void *data; /* 指向临时缓冲区的通用指针 */
int len;

/*
* 获取读锁, 保护全局触发器列表(trigger_list)和本LED的触发器字段.
* 使用读锁允许多个进程同时读取, 提高了并发性能.
*/
down_read(&triggers_list_lock);
down_read(&led_cdev->trigger_lock);

/*
* --- 两遍式策略: 第一遍 (计算大小) ---
* 调用 led_trigger_format, 但传入 NULL 缓冲区和 0 大小.
* 这不会写入任何数据, 而是利用 led_trigger_snprintf 的特性,
* 计算出完整格式化字符串所需要的确切字节数.
*/
len = led_trigger_format(NULL, 0, led_cdev);
/*
* 根据计算出的大小, 分配一个足够大的临时内核内存缓冲区.
* kvmalloc 是一个智能分配器, 对小请求使用 kmalloc, 对大请求使用 vmalloc.
*/
data = kvmalloc(len + 1, GFP_KERNEL);
if (!data) {
/* 内存分配失败, 释放锁并返回错误 */
up_read(&led_cdev->trigger_lock);
up_read(&triggers_list_lock);
return -ENOMEM;
}
/*
* --- 两遍式策略: 第二遍 (实际格式化) ---
* 再次调用 led_trigger_format, 但这次传入了分配好的缓冲区.
* 这会将格式化好的字符串实际写入到 'data' 缓冲区中.
*/
len = led_trigger_format(data, len + 1, led_cdev);

/*
* 数据已安全复制到本地缓冲区, 可以立即释放锁, 减小锁的持有时间.
*/
up_read(&led_cdev->trigger_lock);
up_read(&triggers_list_lock);

/*
* 这是二进制属性的核心. memory_read_from_buffer 是一个库函数,
* 它负责处理用户空间的 read() 系统调用请求.
* 它会从我们的临时缓冲区 'data' 中, 根据用户请求的偏移量 'pos' 和
* 数量 'count', 安全地复制数据到用户缓冲区 'buf'.
* 这使得用户可以像读取普通文件一样, 分块读取这个可能很大的列表.
*/
len = memory_read_from_buffer(buf, count, &pos, data, len);

/* 释放临时缓冲区 */
kvfree(data);

/* 返回实际复制给用户的字节数 */
return len;
}
/* 导出此符号, 使其可被 sysfs 核心调用. */
EXPORT_SYMBOL_GPL(led_trigger_read);

LED sysfs 触发器(Trigger)写入实现

此代码片段实现了用户通过sysfs文件系统设置一个LED”触发器”的功能, 对应于echo "timer" > /sys/class/leds/.../trigger这样的命令。其核心原理是将用户写入的字符串与一个全局的、已注册的触发器列表进行匹配, 并在找到匹配项后, 调用该触发器的activate函数, 将LED的控制权移交给这个触发器


trigger_relevant: 触发器相关性过滤器

这是一个小型的内联辅助函数, 它的作用是在庞大的触发器列表中, 筛选出那些与当前特定LED设备兼容或相关的触发器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* 静态内联函数: trigger_relevant
* @led_cdev: 目标LED设备.
* @trig: 待检查的触发器.
* @return: 如果'trig'与'led_cdev'相关, 返回true, 否则返回false.
*/
static inline bool
trigger_relevant(struct led_classdev *led_cdev, struct led_trigger *trig)
{
/*
* 核心逻辑:
* 1. !trig->trigger_type: 检查此触发器是否有一个特定的类型. 如果没有(为0或NULL),
* 说明它是一个通用触发器, 适用于所有类型的LED. 表达式为true.
* 2. trig->trigger_type == led_cdev->trigger_type: 如果触发器有一个特定类型
* (例如, 'LED_FUNCTION_BACKLIGHT'), 则检查它是否与当前LED设备声明的类型相匹配.
*
* 使用 '||' (或) 连接, 意味着只要满足上述两个条件之一, 该触发器就被认为是相关的.
*/
return !trig->trigger_type || trig->trigger_type == led_cdev->trigger_type;
}

led_trigger_write: trigger文件的写操作回调

这是当用户空间向trigger文件写入数据时, 内核调用的主函数。它负责解析用户的意图并执行相应的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*
* led_trigger_write: 'trigger'二进制属性的写回调函数.
*/
ssize_t led_trigger_write(struct file *filp, struct kobject *kobj,
const struct bin_attribute *bin_attr, char *buf,
loff_t pos, size_t count)
{
/* 从 kobject 获取 device, 再从 device 获取 led_classdev 私有数据 */
struct device *dev = kobj_to_dev(kobj);
struct led_classdev *led_cdev = dev_get_drvdata(dev);
struct led_trigger *trig;
int ret = count; /* 默认情况下, 假设成功处理了所有写入的字节 */

/*
* 获取针对此LED设备的互斥锁.
* 这确保了在修改触发器的整个过程中, 不会有其他线程(如读/写brightness)
* 同时访问 led_cdev, 保证了操作的原子性.
*/
mutex_lock(&led_cdev->led_access);

/* 检查LED当前是否被其他机制(如另一个触发器)锁定, 禁止sysfs修改 */
if (led_sysfs_is_disabled(led_cdev)) {
ret = -EBUSY; /* 返回"设备或资源忙"错误 */
goto unlock;
}

/*
* 处理特殊关键字: "none"
* sysfs_streq 是一个安全的字符串比较函数, 它能正确处理用户输入末尾可能存在的换行符.
*/
if (sysfs_streq(buf, "none")) {
/* 如果用户写入"none", 则移除当前绑定的所有触发器, 让LED恢复手动控制. */
led_trigger_remove(led_cdev);
goto unlock;
}

/* 处理特殊关键字: "default" */
if (sysfs_streq(buf, "default")) {
/* 如果用户写入"default", 则激活此LED在设备树或驱动中预设的默认触发器. */
led_trigger_set_default(led_cdev);
goto unlock;
}

/*
* 如果不是特殊关键字, 则开始在全局触发器列表中搜索匹配项.
* 获取全局触发器列表的读锁. 使用读锁是因为我们只是遍历列表, 不修改它,
* 这允许多个进程同时进行读取操作, 提高了并发性.
*/
down_read(&triggers_list_lock);
/* 遍历全局的 trigger_list 链表. */
list_for_each_entry(trig, &trigger_list, next_trig) {
/*
* 检查两个条件:
* 1. 用户写入的字符串(buf)是否与当前遍历到的触发器名称(trig->name)匹配.
* 2. 这个触发器是否与当前LED相关 (通过我们上面分析的 trigger_relevant 函数).
*/
if (sysfs_streq(buf, trig->name) && trigger_relevant(led_cdev, trig)) {
/*
* 找到了一个完全匹配的触发器.
* 现在需要修改 led_cdev->trigger 字段, 因此需要获取该字段的写锁.
*/
down_write(&led_cdev->trigger_lock);
/*
* 调用 led_trigger_set, 这个核心函数会:
* 1. 如果有旧触发器, 调用其 deactivate 回调.
* 2. 将 led_cdev->trigger 指向新的触发器 trig.
* 3. 调用新触发器 trig 的 activate 回调, 将LED的控制权正式移交.
*/
led_trigger_set(led_cdev, trig);
up_write(&led_cdev->trigger_lock); /* 释放写锁 */

up_read(&triggers_list_lock); /* 释放读锁 */
goto unlock; /* 操作成功, 跳转到结尾 */
}
}
/*
* 如果整个循环结束都没有找到匹配的触发器, 说明用户输入了一个无效的名称.
*/
ret = -EINVAL; /* 设置返回值为"无效参数"错误 */
up_read(&triggers_list_lock); /* 释放读锁 */

unlock:
mutex_unlock(&led_cdev->led_access); /* 释放外层互斥锁 */
return ret; /* 返回操作结果 */
}
/* 导出此符号, 使其可被 sysfs 核心调用. */
EXPORT_SYMBOL_GPL(led_trigger_write);

led_trigger_format: 格式化触发器列表字符串

这个辅助函数负责构建用户最终看到的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*
* led_trigger_format: 将一个LED的可用触发器列表格式化到缓冲区中.
* @return: 格式化后的字符串长度.
*/
static int led_trigger_format(char *buf, size_t size,
struct led_classdev *led_cdev)
{
struct led_trigger *trig;
/*
* 开始格式化: 首先添加 "none" 选项.
* 如果当前没有触发器被激活 (led_cdev->trigger 为 NULL),
* 则 "none" 会被方括号括起来, 形式为 "[none]".
* 否则, 形式为 "none".
*/
int len = led_trigger_snprintf(buf, size, "%s",
led_cdev->trigger ? "none" : "[none]");

/* 如果此LED有一个默认的触发器, 在列表后面追加 " default" 提示. */
if (led_cdev->default_trigger)
len += led_trigger_snprintf(buf + len, size - len, " default");

/*
* 遍历全局的 trigger_list 链表, 该链表包含了所有已注册的触发器.
*/
list_for_each_entry(trig, &trigger_list, next_trig) {
bool hit;

/* 检查此触发器是否与当前LED相关. (例如, 'ide-disk'触发器与无IDE的系统无关) */
if (!trigger_relevant(led_cdev, trig))
continue;

/*
* 检查当前遍历到的触发器 'trig' 是否就是此LED正被激活的触发器.
* 'hit' 为 true 表示匹配.
*/
hit = led_cdev->trigger && !strcmp(led_cdev->trigger->name, trig->name);

/*
* 将触发器名称追加到字符串中.
* 如果 'hit' 为 true, 则用方括号将名称括起来.
* 例如, " timer", "[heartbeat]", " disk-activity".
*/
len += led_trigger_snprintf(buf + len, size - len,
" %s%s%s", hit ? "[" : "",
trig->name, hit ? "]" : "");
}

/* 在字符串末尾添加换行符. */
len += led_trigger_snprintf(buf + len, size - len, "\n");

return len;
}

led_trigger_snprintf: 安全的格式化打印工具

这是一个小型的辅助函数, 它是实现”两遍式”策略的关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* __printf(3, 4) 是一个GCC属性, 它告诉编译器像检查printf一样,
* 检查本函数的第3个参数(fmt)和第4个及之后的参数(...)的类型匹配性.
*/
__printf(3, 4)
static int led_trigger_snprintf(char *buf, ssize_t size, const char *fmt, ...)
{
va_list args;
int i;

va_start(args, fmt); /* 初始化可变参数列表 */
/* 核心逻辑: */
if (size <= 0)
/*
* 如果提供的缓冲区大小无效, 就调用 vsnprintf(NULL, 0, ...)
* 这是一个标准技巧, 它不会写入任何数据, 而是返回格式化完整的字符串所需要的字节数.
*/
i = vsnprintf(NULL, 0, fmt, args);
else
/*
* 否则, 调用 vscnprintf, 这是一个安全的打印函数,
* 它会向 buf 中最多写入 size 个字节, 并返回实际写入的字节数.
*/
i = vscnprintf(buf, size, fmt, args);
va_end(args); /* 清理可变参数列表 */

return i;
}

LED 触发器(Trigger)管理核心

此代码片段是Linux内核LED子系统中负责动态管理”触发器”(Trigger)的核心逻辑。触发器是一种机制, 它允许一个内核子系统(如定时器、磁盘I/O、CPU负载)接管一个LED的控制权, 以自动地、有规律地改变其状态来反映系统事件。这些函数共同实现了设置、移除和恢复默认触发器的完整生命周期管理。

其核心原理是通过一个”先拆后建”(Teardown-then-Setup)的原子操作, 安全地将LED的控制权从一个触发器转移到另一个, 同时处理好资源清理、sysfs接口更新以及与用户空间的通信


led_trigger_set: 核心引擎 - 设置或移除一个触发器

这个函数是所有触发器切换操作的中心。它被设计为一个多功能的函数: 当传入一个有效的trig指针时, 它会激活该触发器; 当传入NULL时, 它会移除当前活动的触发器。

  • 原理 - 停用(Teardown)阶段: 如果当前已有触发器(led_cdev->trigger不为NULL), 此函数会执行一系列严格的清理步骤:

    1. 从列表中移除: 使用RCU(Read-Copy-Update)安全地将LED设备从旧触发器的控制列表中移除。synchronize_rcu()确保了在继续之前, 所有可能正在读取该列表的代码都已经完成。
    2. 取消挂起工作: cancel_work_sync()确保任何由用户空间写入brightness而排队的异步工作被取消, 防止旧的状态改变干扰新触发器。
    3. 停止软件闪烁: 如果LED正由定时器控制闪烁, 停止它。
    4. 移除sysfs属性: 如果旧触发器添加了自己特有的sysfs文件(例如, “timer”触发器的delay_on/delay_off), 则将它们移除。
    5. 调用deactivate回调: 调用旧触发器自身的deactivate()函数, 让触发器有机会释放它所占用的资源。
    6. 状态复位: 将LED的状态完全重置: trigger指针设为NULL, 关闭LED。
  • 原理 - 激活(Setup)阶段: 如果传入的新trig不为NULL, 则执行激活步骤:

    1. 添加到列表: 使用RCU将LED设备添加到新触发器的控制列表中。
    2. 调用activate回调: 调用新触发器的activate()函数, 这是将LED控制权正式移交的关键一步。触发器从此开始根据其内部逻辑控制LED。
    3. 添加sysfs属性: 如果新触发器有特有的sysfs文件, 将它们添加到此LED的sysfs目录中。
    4. 健壮的错误处理: 如果激活或添加sysfs属性的任何一步失败, 函数会执行一个完整的”回滚”操作, 即再次执行停用流程, 确保LED处于一个干净、无触发器的状态。
  • 原理 - 通知用户空间: 无论操作是设置还是移除, 最后都会调用kobject_uevent_env发送一个uevent事件。这会通知用户空间的守护进程(如udev), trigger文件的内容已经发生了变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/* 调用者必须持有 led_cdev->trigger_lock 的写锁 */
int led_trigger_set(struct led_classdev *led_cdev, struct led_trigger *trig)
{
char *event = NULL;
char *envp[2];
const char *name;
int ret;
if (!led_cdev->trigger && !trig)
return 0;

name = trig ? trig->name : "none";
event = kasprintf(GFP_KERNEL, "TRIGGER=%s", name);

/* --- 停用(Teardown)阶段 --- */
if (led_cdev->trigger) {
/* 从旧触发器的控制列表中安全移除本LED */
spin_lock(&led_cdev->trigger->leddev_list_lock);
list_del_rcu(&led_cdev->trig_list);
spin_unlock(&led_cdev->trigger->leddev_list_lock);
synchronize_rcu(); // 等待所有读者完成

/* 清理所有与旧触发器相关的状态和资源 */
cancel_work_sync(&led_cdev->set_brightness_work);
led_stop_software_blink(led_cdev);
device_remove_groups(led_cdev->dev, led_cdev->trigger->groups);
if (led_cdev->trigger->deactivate)
led_cdev->trigger->deactivate(led_cdev);

/* 重置LED状态 */
led_cdev->trigger = NULL;
// ... [其他状态重置] ...
led_cdev->trigger = NULL;
led_cdev->trigger_data = NULL;
led_cdev->activated = false;
led_cdev->flags &= ~LED_INIT_DEFAULT_TRIGGER;
led_set_brightness(led_cdev, LED_OFF);
}

/* --- 激活(Setup)阶段 --- */
if (trig) {
/* 将本LED添加到新触发器的控制列表 */
spin_lock(&trig->leddev_list_lock);
list_add_tail_rcu(&led_cdev->trig_list, &trig->led_cdevs);
spin_unlock(&trig->leddev_list_lock);
led_cdev->trigger = trig;

synchronize_rcu(); // 确保LED在列表中可见

/* 刷新挂起工作, 防止与activate()冲突 */
flush_work(&led_cdev->set_brightness_work);

/* 调用新触发器的激活函数 */
if (trig->activate)
ret = trig->activate(led_cdev);
// ... [错误处理和添加sysfs属性] ...
ret = 0;
if (trig->activate)
ret = trig->activate(led_cdev);
else
led_set_brightness(led_cdev, trig->brightness);
if (ret)
goto err_activate;

ret = device_add_groups(led_cdev->dev, trig->groups);
if (ret) {
dev_err(led_cdev->dev, "Failed to add trigger attributes\n");
goto err_add_groups;
}
}

/* --- 通知用户空间 --- */
if (event) {
// ... [发送 uevent] ...
envp[0] = event;
envp[1] = NULL;
if (kobject_uevent_env(&led_cdev->dev->kobj, KOBJ_CHANGE, envp))
dev_err(led_cdev->dev,
"%s: Error sending uevent\n", __func__);
kfree(event);
}

return 0;

/* --- 错误回滚路径 --- */
err_add_groups:
if (trig->deactivate)
trig->deactivate(led_cdev);
err_activate:
// ... [完整的停用和状态重置逻辑] ...
return ret;
}
EXPORT_SYMBOL_GPL(led_trigger_set);

led_trigger_removeled_trigger_set_default: 便捷的API封装

这两个函数是提供给其他内核代码使用的上层API, 它们通过加锁并调用led_trigger_set来简化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
* led_trigger_remove: 移除一个LED上当前活动的触发器.
* 这是一个简单的封装, 它获取写锁, 然后调用核心函数并传入NULL.
*/
void led_trigger_remove(struct led_classdev *led_cdev)
{
down_write(&led_cdev->trigger_lock);
led_trigger_set(led_cdev, NULL);
up_write(&led_cdev->trigger_lock);
}
EXPORT_SYMBOL_GPL(led_trigger_remove);

static bool led_match_default_trigger(struct led_classdev *led_cdev,
struct led_trigger *trig)
{
if (!strcmp(led_cdev->default_trigger, trig->name) &&
trigger_relevant(led_cdev, trig)) {
led_cdev->flags |= LED_INIT_DEFAULT_TRIGGER;
led_trigger_set(led_cdev, trig);
return true;
}

return false;
}
/*
* led_trigger_set_default: 为一个LED设置其预定义的默认触发器.
* 默认触发器的名字通常在设备树中指定.
*/
void led_trigger_set_default(struct led_classdev *led_cdev)
{
// ... [变量定义和'none'特殊情况处理] ...
struct led_trigger *trig;
bool found = false;

if (!led_cdev->default_trigger)
return;

if (!strcmp(led_cdev->default_trigger, "none")) {
led_trigger_remove(led_cdev);
return;
}

/* 遍历全局触发器列表, 寻找与 default_trigger 名字匹配的项 */
down_read(&triggers_list_lock);
down_write(&led_cdev->trigger_lock);
list_for_each_entry(trig, &trigger_list, next_trig) {
/* led_match_default_trigger 会检查名字和相关性, 并调用 led_trigger_set */
found = led_match_default_trigger(led_cdev, trig);
if (found)
break;
}
up_write(&led_cdev->trigger_lock);
up_read(&triggers_list_lock);

/*
* 核心的模块化设计: 如果在当前已加载的触发器中没找到,
* 就请求内核去异步加载一个可能提供该触发器的内核模块.
* 模块加载后, 会重新检查并设置触发器.
*/
if (!found)
request_module_nowait("ledtrig:%s", led_cdev->default_trigger);
}
EXPORT_SYMBOL_GPL(led_trigger_set_default);

LED子系统核心接口:亮度、闪烁与触发器事件处理

本代码片段摘自Linux内核的LED Class子系统,提供了控制LED设备行为的核心API。其主要功能包括:设置LED的亮度、控制LED的闪烁(包括异步处理和软件模拟闪烁),以及处理来自LED触发器(Trigger)的事件。这段代码是连接上层应用(通过sysfs)或内核其他子系统(通过API调用)与底层具体LED硬件驱动之间的关键桥梁。

实现原理分析

该代码的实现体现了Linux设备驱动模型中的分层和抽象思想,并巧妙地利用内核机制来处理并发和休眠上下文问题。

  1. 异步操作与工作队列 (Work Queue): led_blink_set_nosleepled_set_brightness 函数都可能需要执行可能休眠的操作(例如,调用一个底层驱动提供的brightness_set_blocking函数,或同步删除一个定时器timer_delete_sync)。为了让这些API可以安全地从中断等原子上下文中调用,它们采用了一种延迟执行的策略。它们并不直接执行操作,而是设置一个标志位(work_flags),然后将一个预先准备好的工作项(set_brightness_work)添加到工作队列中。内核的工作队列线程稍后会在一个可以安全休眠的进程上下文中执行实际的工作。

  2. 软件闪烁 (Software Blink): 对于没有硬件闪烁功能的LED,内核通过一个高精度定时器(blink_timer)来模拟闪烁。led_stop_software_blink 函数负责停止这种模拟。led_set_brightness 在软件闪烁激活时,不会立即改变亮度,而是更新一个目标亮度值,并设置标志位,由定时器的下一次回调函数来应用新的亮度,从而避免与定时器的闪烁逻辑产生冲突。

  3. 触发器 (Triggers): led_trigger_event 函数实现了LED触发器机制。触发器是内核中定义的事件源(如磁盘活动、CPU负载、网络流量等)。一个或多个LED设备可以注册到同一个触发器上。当led_trigger_event被调用时,它会遍历所有注册到该触发器的LED设备,并统一设置它们的亮度。

  4. RCU (Read-Copy-Update): led_trigger_event 在遍历与触发器关联的LED设备列表时,使用了RCU机制(rcu_read_lock/unlocklist_for_each_entry_rcu)。RCU是一种高效的并发控制机制,它允许在没有任何锁的情况下安全地读取一个可能被其他CPU修改的链表。这对于性能敏感的事件(如网络包到达)触发LED闪烁的场景至关重要。

代码分析

LED闪烁与亮度设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// led_blink_set_nosleep: 设置LED闪烁的亮/灭时间(非休眠版本)。
void led_blink_set_nosleep(struct led_classdev *led_cdev, unsigned long delay_on,
unsigned long delay_off)
{
/* 如果底层驱动的设置函数是阻塞的,则将任务委托给工作队列。*/
if (led_cdev->blink_set && led_cdev->brightness_set_blocking) {
// 暂存亮/灭时间。
led_cdev->delayed_delay_on = delay_on;
led_cdev->delayed_delay_off = delay_off;
// 设置标志位,告知工作队列需要执行闪烁设置。
set_bit(LED_SET_BLINK, &led_cdev->work_flags);
// 将工作项加入工作队列等待调度。
queue_work(led_cdev->wq, &led_cdev->set_brightness_work);
return;
}

// 如果底层驱动是非阻塞的,则直接调用闪烁设置函数。
led_blink_set(led_cdev, &delay_on, &delay_off);
}
EXPORT_SYMBOL_GPL(led_blink_set_nosleep);

// led_stop_software_blink: 停止由内核定时器模拟的软件闪烁。
void led_stop_software_blink(struct led_classdev *led_cdev)
{
// 同步删除闪烁定时器,确保返回时定时器回调已不再运行。
timer_delete_sync(&led_cdev->blink_timer);
// 清零闪烁时间参数。
led_cdev->blink_delay_on = 0;
led_cdev->blink_delay_off = 0;
// 清除软件闪烁活动标志。
clear_bit(LED_BLINK_SW, &led_cdev->work_flags);
}
EXPORT_SYMBOL_GPL(led_stop_software_blink);

// led_set_brightness: 设置LED亮度(上层通用API)。
void led_set_brightness(struct led_classdev *led_cdev, unsigned int brightness)
{
/*
* 如果软件闪烁当前是激活的,则延迟亮度的设置,
* 直到下一个定时器滴答。
*/
if (test_bit(LED_BLINK_SW, &led_cdev->work_flags)) {
/*
* 如果需要关闭LED(亮度为0),则需要禁用软件闪烁。
* 因为停止定时器可能休眠,所以将此任务委托给工作队列。
*/
if (!brightness) {
set_bit(LED_BLINK_DISABLE, &led_cdev->work_flags);
queue_work(led_cdev->wq, &led_cdev->set_brightness_work);
} else {
// 如果只是改变闪烁时的亮度,则设置标志并保存新的亮度值。
// 定时器回调函数会在下一次触发时应用这个新亮度。
set_bit(LED_BLINK_BRIGHTNESS_CHANGE,
&led_cdev->work_flags);
led_cdev->new_blink_brightness = brightness;
}
return;
}

// 如果没有软件闪烁,则直接调用非休眠版本的亮度设置函数。
led_set_brightness_nosleep(led_cdev, brightness);
}
EXPORT_SYMBOL_GPL(led_set_brightness);

LED触发器事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// led_trigger_event: 触发一个事件,改变所有关联LED的亮度。
void led_trigger_event(struct led_trigger *trig,
enum led_brightness brightness)
{
struct led_classdev *led_cdev;

if (!trig)
return;

// 更新触发器的亮度状态
trig->brightness = brightness;

// 进入RCU读侧临界区,以安全地遍历LED设备列表。
rcu_read_lock();
// 遍历所有注册到此触发器的LED设备。
list_for_each_entry_rcu(led_cdev, &trig->led_cdevs, trig_list)
// 为每一个关联的LED设置亮度。
led_set_brightness(led_cdev, brightness);
// 退出RCU读侧临界区。
rcu_read_unlock();
}
EXPORT_SYMBOL_GPL(led_trigger_event);

drivers/leds/led-class.c

LED sysfs 属性接口

此代码片段定义了所有注册到LED子系统的设备在sysfs中暴露给用户空间的文件接口。其核心原理是将用户对sysfs文件的读写操作, 通过一系列的回调函数, 转换成对具体LED设备(led_classdev)数据结构的操作, 并最终触发硬件动作。这是Linux内核驱动模型中一个典型的、将内核功能暴露给用户空间的实现范例。


brightness 属性 (读/写)

这是控制LED亮度最核心的接口, 对应sysfs中的brightness文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/*
* brightness_show: 当用户 'cat /sys/class/leds/.../brightness' 时被调用.
* @dev: 指向此LED设备的通用 struct device 指针.
* @attr: 描述此属性的结构体.
* @buf: 内核提供的缓冲区, 用于存放要返回给用户空间的数据.
* @return: 写入缓冲区的字节数.
*/
static ssize_t brightness_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
/* 从通用设备指针中获取LED驱动的私有数据结构. */
struct led_classdev *led_cdev = dev_get_drvdata(dev);
unsigned int brightness;

/*
* 加锁以保护对 led_cdev 的并发访问.
* 防止在读取亮度的同时, 有其他进程或内核触发器(trigger)正在修改它.
*/
mutex_lock(&led_cdev->led_access);
/*
* 如果有触发器在控制LED, 此函数可能会更新 brightness 成员以反映触发器的当前状态.
*/
led_update_brightness(led_cdev);
brightness = led_cdev->brightness;
mutex_unlock(&led_cdev->led_access);

/* 使用 sprintf 将亮度值格式化为字符串, 存入 buf 并返回给用户. */
return sprintf(buf, "%u\n", brightness);
}

/*
* brightness_store: 当用户 'echo "128" > /sys/class/leds/.../brightness' 时被调用.
* @dev: 设备指针.
* @attr: 属性描述.
* @buf: 包含了用户写入的数据的缓冲区.
* @size: 写入数据的字节数.
* @return: 成功处理的字节数, 或错误码.
*/
static ssize_t brightness_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t size)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
unsigned long state;
ssize_t ret;

mutex_lock(&led_cdev->led_access);

/* 检查LED当前是否被触发器控制. 如果是, 则用户不能手动修改亮度. */
if (led_sysfs_is_disabled(led_cdev)) {
ret = -EBUSY; /* 返回"设备或资源忙"错误 */
goto unlock;
}

/* 使用 kstrtoul 将用户传入的字符串转换为无符号长整型. */
ret = kstrtoul(buf, 10, &state);
if (ret)
goto unlock;

/* 这是一个特殊策略: 如果用户写入0, 不仅设置亮度为0, 还会移除当前激活的触发器. */
if (state == LED_OFF)
led_trigger_remove(led_cdev);
/*
* 调用核心函数 led_set_brightness. 这个函数最终会调用具体硬件驱动
* 注册的 .brightness_set() 回调函数来实际改变硬件LED的亮度.
*/
led_set_brightness(led_cdev, state);

ret = size; /* 成功, 返回已处理的字节数. */
unlock:
mutex_unlock(&led_cdev->led_access);
return ret;
}
/*
* DEVICE_ATTR_RW 是一个宏, 它会自动创建一个名为 dev_attr_brightness 的
* struct device_attribute 实例, 并将它的 .show 和 .store 成员
* 分别指向上面定义的 brightness_show 和 brightness_store 函数.
* 并且, 它会将文件权限设置为 0644 (所有者可读写, 其他人只读).
*/
static DEVICE_ATTR_RW(brightness);

max_brightness 属性 (只读)

这个接口用于查询LED硬件支持的最大亮度值, 对应sysfs中的max_brightness文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* max_brightness_show: 当用户 'cat /sys/class/leds/.../max_brightness' 时被调用.
*/
static ssize_t max_brightness_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct led_classdev *led_cdev = dev_get_drvdata(dev);
unsigned int max_brightness;

mutex_lock(&led_cdev->led_access);
/* max_brightness 通常是在驱动探测时设置的一个常量. */
max_brightness = led_cdev->max_brightness;
mutex_unlock(&led_cdev->led_access);

return sprintf(buf, "%u\n", max_brightness);
}
/*
* DEVICE_ATTR_RO 宏与 DEVICE_ATTR_RW 类似, 但它只接受一个 'show' 函数,
* 因此创建的 sysfs 文件是只读的 (权限 0444).
*/
static DEVICE_ATTR_RO(max_brightness);

trigger 属性与属性组装

这部分代码负责组装上述定义的所有属性, 并有条件地包含”触发器”(trigger)功能, 最后将它们打包成内核设备模型可以理解的格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
* 只有当内核配置了 CONFIG_LEDS_TRIGGERS 时, 这部分代码才会被编译.
* 这是一种常见的条件编译, 用于使内核功能模块化.
*/
#ifdef CONFIG_LEDS_TRIGGERS
/*
* BIN_ATTR 宏用于创建二进制(binary) sysfs 属性. 与DEVICE_ATTR不同,
* 它的读写函数直接操作原始的二进制缓冲区, 提供了更大的灵活性.
* trigger 接口需要列出所有可用触发器并设置其中一个, 使用二进制接口更方便.
*/
static const BIN_ATTR(trigger, 0644, led_trigger_read, led_trigger_write, 0);

/* 定义一个包含所有二进制属性的数组 (这里只有一个). */
static const struct bin_attribute *const led_trigger_bin_attrs[] = {
&bin_attr_trigger,
NULL, /* 数组必须以NULL结尾. */
};
/* 将二进制属性数组打包成一个 "属性组". */
static const struct attribute_group led_trigger_group = {
.bin_attrs = led_trigger_bin_attrs,
};
#endif

/*
* 定义一个包含所有普通(文本)属性的数组.
* .attr 是 DEVICE_ATTR_* 宏创建的结构体中的一个成员.
*/
static struct attribute *led_class_attrs[] = {
&dev_attr_brightness.attr,
&dev_attr_max_brightness.attr,
NULL, /* 数组必须以NULL结尾. */
};

/* 将普通属性数组打包成一个 "属性组". */
static const struct attribute_group led_group = {
.attrs = led_class_attrs,
};

/*
* 这是最终的属性组列表, 它将被 'leds_class' 使用.
* 这是一个指向属性组的指针数组.
*/
static const struct attribute_group *led_groups[] = {
&led_group, /* 包含 brightness 和 max_brightness 的基本组. */
#ifdef CONFIG_LEDS_TRIGGERS
&led_trigger_group, /* 如果配置了触发器, 则也包含 trigger 属性组. */
#endif
NULL, /* 数组必须以NULL结尾. */
};

LED类定义及模块安全交互

此代码片段定义了Linux内核中”leds”设备类的核心结构, 并提供了一个关键的辅助函数, 用于在与具体LED设备交互时, 安全地管理其父驱动模块的生命周期。

  1. leds_class: 这是LED子系统的核心。它是一个struct class实例, 负责在sysfs中创建/sys/class/leds目录。它将所有LED设备驱动注册的设备聚合在一起, 并通过.dev_groups为每个设备提供一套标准的sysfs属性(如brightness, trigger等)。它还通过.pm指针关联了电源管理操作, 使得内核的电源管理核心可以统一地对所有LED设备进行挂起(suspend)和恢复(resume)操作。
  2. SIMPLE_DEV_PM_OPS: 这是一个辅助宏, 用于快速定义一个简单的dev_pm_ops结构体。在这里, 它创建了leds_class_dev_pm_ops实例, 并将其中的.suspend.resume回调分别指向了led_suspendled_resume函数。这是一种简化驱动代码的常用技巧。
  3. led_module_get: 这是一个至关重要的辅助函数, 其核心原理是防止因内核模块卸载而引发的”use-after-free”内核恐慌。当用户通过sysfs与一个LED设备交互时, LED子系统需要调用其物理父设备(例如, 一个I2C IO扩展器)的驱动程序所提供的函数。如果在此时, 该父驱动模块正在被卸载(rmmod), 其代码和数据结构可能已经被释放。led_module_get通过调用try_module_get()来尝试增加父驱动模块的引用计数。如果成功, 它就获得了一个”锁”, 保证了父模块在此次操作完成前不会被卸载; 如果失败, 则说明父模块正在被卸载, 函数会安全地返回错误, 从而避免了对无效内存的访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/*
* SIMPLE_DEV_PM_OPS 是一个辅助宏, 用于快速定义一个 struct dev_pm_ops 实例.
* - 第一个参数 leds_class_dev_pm_ops 是将要创建的结构体实例的名称.
* - 第二个参数 led_suspend 是一个函数指针, 它将被赋值给结构体中的 .suspend 成员.
* - 第三个参数 led_resume 是一个函数指针, 它将被赋值给结构体中的 .resume 成员.
* 这行代码的作用是为LED类设备创建一个标准的电源管理操作集.
*/
static SIMPLE_DEV_PM_OPS(leds_class_dev_pm_ops, led_suspend, led_resume);

/*
* led_module_get: 从一个通用设备指针安全地获取 led_classdev 实例, 并锁定其父模块.
* @led_dev: 指向LED设备(在/sys/class/leds/下)的 struct device 指针.
* @return: 成功时返回 led_classdev 指针; 失败时返回错误指针.
*/
static struct led_classdev *led_module_get(struct device *led_dev)
{
struct led_classdev *led_cdev;

/*
* 检查传入的设备指针是否有效. 如果无效, 可能是因为依赖的设备尚未探测成功.
* 返回 -EPROBE_DEFER 会告知内核驱动核心, 稍后重试此操作.
*/
if (!led_dev)
return ERR_PTR(-EPROBE_DEFER);

/*
* 从通用设备结构体中获取驱动的私有数据, 这里就是 led_classdev 结构体本身.
* 这个数据是在注册LED设备时通过 dev_set_drvdata() 设置的.
*/
led_cdev = dev_get_drvdata(led_dev);

/*
* 这是本函数最关键的部分, 用于防止模块卸载竞争.
* 1. led_cdev->dev->parent: 获取LED设备的物理父设备 (例如, I2C控制器上的IO扩展器芯片).
* 2. ->driver: 获取父设备的驱动程序.
* 3. ->owner: 获取拥有该驱动的内核模块.
* 4. try_module_get(): 尝试增加该内核模块的引用计数.
* 如果该模块正在被卸载, 此函数会失败并返回false.
*/
if (!try_module_get(led_cdev->dev->parent->driver->owner)) {
/*
* 如果增加引用计数失败, 意味着父设备驱动即将消失.
* 此时不能继续操作. 调用 put_device() 释放对led_dev的引用.
*/
put_device(led_cdev->dev);
/*
* 返回 -ENODEV ("无此设备"), 因为提供服务的物理设备已经不可用.
*/
return ERR_PTR(-ENODEV);
}

/*
* 如果 try_module_get() 成功, 我们就获得了一个保证: 在我们调用 module_put() 之前,
* 父驱动模块不会被卸载. 现在可以安全地返回 led_cdev 指针供后续使用.
*/
return led_cdev;
}

/*
* 定义一个静态常量 struct class 实例, 描述 "leds" 这个设备类.
* 'const' 意味着它在编译后是只读数据.
*/
static const struct class leds_class = {
/*
* .name = "leds":
* 这是类的名称. 内核会根据这个名字在 sysfs 中创建 /sys/class/leds/ 目录.
*/
.name = "leds",
/*
* .dev_groups = led_groups:
* 这是一个指针, 指向一个属性组(attribute group)数组.
* 这个数组定义了所有属于本类的设备在 sysfs 中应该具有的通用文件属性,
* 例如 "brightness", "max_brightness", "trigger" 等.
* 当一个新LED设备注册时, 内核会自动为它创建这些文件.
*/
.dev_groups = led_groups,
/*
* .pm = &leds_class_dev_pm_ops:
* 这是一个指针, 指向本类的电源管理操作函数集.
* 内核的电源管理核心可以通过这个指针, 调用 led_suspend 和 led_resume
* 来挂起和恢复所有属于 "leds" 类的设备.
*/
.pm = &leds_class_dev_pm_ops,
};

LED子系统初始化与资源管理

此代码片段展示了Linux内核中LED类(Class)子系统的核心初始化、退出以及设备资源管理(devm)的相关实现。其核心原理是建立一个标准化的框架, 包括一个专用的工作队列和一个设备类, 使得所有不同类型的LED设备驱动都能以统一的方式注册到内核, 并向用户空间提供一致的控制接口


leds_initleds_exit: 子系统的生命周期管理

这两个函数负责在内核启动时建立LED子系统的基础架构, 并在内核关闭或模块卸载时安全地拆除它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
* 静态初始化函数.
* __init 宏告诉编译器将此函数放入特殊的".init.text"段,
* 内核启动完成后, 这部分内存可以被释放, 以节省RAM, 这对嵌入式系统至关重要.
*/
static int __init leds_init(void)
{
/*
* 创建一个名为 "leds" 的有序工作队列 (ordered workqueue).
* 工作队列(workqueue)是内核将工作推迟到内核线程上下文中执行的机制.
* 这对于LED操作(如闪烁)至关重要, 因为它们可能涉及延迟, 不应阻塞当前代码路径.
* "有序"(ordered)确保了提交给同一个LED的任务会严格按照提交顺序执行,
* 例如, "开灯"命令总是在"关灯"命令之前完成.
*/
leds_wq = alloc_ordered_workqueue("leds", 0);
if (!leds_wq) {
pr_err("Failed to create LEDs ordered workqueue\n");
return -ENOMEM;
}

/*
* 调用 class_register() 注册一个名为 "leds" 的设备类.
* 这个操作会在 sysfs 文件系统中创建 /sys/class/leds/ 目录.
* 所有后续注册的具体LED设备驱动, 都会在这个目录下创建自己的条目,
* 从而向用户空间提供一个标准化的控制接口 (例如, 控制 brightness, trigger 等).
*/
return class_register(&leds_class);
}

/*
* 静态退出函数.
* __exit 宏表示此函数仅在模块被卸载或内核关闭时才需要,
* 对于静态编译进内核且永不卸载的模块, 编译器可能会优化掉此函数.
*/
static void __exit leds_exit(void)
{
/*
* 注销之前注册的 "leds" 设备类, 清理 /sys/class/leds/ 目录.
*/
class_unregister(&leds_class);
/*
* 销毁工作队列, 确保所有挂起的工作都被处理或取消, 并释放相关资源.
*/
destroy_workqueue(leds_wq);
}

/*
* subsys_initcall() 是一个宏, 用于将 leds_init 函数注册为内核的一个"子系统初始化调用".
* 这确保了 leds_init 函数会在内核启动过程中的一个合适的、较早的阶段被自动调用.
*/
subsys_initcall(leds_init);
/*
* module_exit() 宏指定当本模块被卸载时, leds_exit 函数应该被调用.
*/
module_exit(leds_exit);

devm_led_classdev_unregisterdevm_led_classdev_match: 资源管理

这两个函数是”设备资源管理”(devm)框架的一部分, 旨在简化驱动程序的资源清理工作。devm的核心思想是将资源的生命周期与设备的生命周期绑定, 当设备被移除或驱动卸载时, 由devm框架自动释放所有已注册的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
* 静态函数: devm_led_classdev_match
* 这是一个匹配函数, 作为 devres_release 的回调.
* @dev: 设备指针.
* @res: 指向设备资源列表中某个资源的指针 (在这里是 'struct led_classdev **').
* @data: 用户传入的、需要匹配的数据 (在这里是 'struct led_classdev *').
* @return: 如果 res 指向的资源与 data 匹配, 返回 1, 否则返回 0.
*/
static int devm_led_classdev_match(struct device *dev, void *res, void *data)
{
/* 将通用的 res 指针转换为其真实类型: 指向 led_classdev 指针的指针 */
struct led_classdev **p = res;

/* 健全性检查: 如果指针无效, 发出警告并返回不匹配. */
if (WARN_ON(!p || !*p))
return 0;

/* 核心逻辑: 比较资源列表中存储的 led_classdev 指针 (*p) 与我们要查找的指针 (data) 是否相同. */
return *p == data;
}

/**
* devm_led_classdev_unregister() - led_classdev_unregister() 的资源管理版本
* @dev: 要注销LED的设备.
* @led_cdev: 要注销的 led_classdev 结构体.
*/
void devm_led_classdev_unregister(struct device *dev,
struct led_classdev *led_cdev)
{
/*
* 调用 devres_release() 在 dev 设备的资源列表中查找并释放一个特定的资源.
* - devm_led_classdev_release: 找到匹配资源后, devres_release 会调用这个函数来执行实际的清理工作(即 led_classdev_unregister).
* - devm_led_classdev_match: devres_release 使用这个函数来判断列表中的每个资源是否是我们要找的那个.
* - led_cdev: 这是传递给匹配函数的数据, 用于比较.
* WARN_ON() 检查 devres_release 的返回值, 如果释放失败(通常不应该发生), 则打印内核警告.
*/
WARN_ON(devres_release(dev,
devm_led_classdev_release,
devm_led_classdev_match, led_cdev));
}
/* 导出符号, 使此函数可被其他内核模块调用. */
EXPORT_SYMBOL_GPL(devm_led_classdev_unregister);

模块元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* MODULE_AUTHOR: 声明模块的作者.
*/
MODULE_AUTHOR("John Lenz, Richard Purdie");
/*
* MODULE_LICENSE: 声明模块的许可证. "GPL" 表示它遵循GNU通用公共许可证.
* 这是内核模块最常见的许可证, 也是使用GPL导出符号(EXPORT_SYMBOL_GPL)的前提.
*/
MODULE_LICENSE("GPL");
/*
* MODULE_DESCRIPTION: 提供对模块功能的简短描述.
* 这些信息可以通过 `modinfo` 等用户空间工具查看.
*/
MODULE_DESCRIPTION("LED Class Interface");

GPIO LED 驱动:Probe Logic, Backward Compatibility, and Shutdown

本代码片段是 leds-gpio 驱动的核心部分,展示了其探测(probe)逻辑、强大的向后兼容性设计以及关机(shutdown)处理gpio_led_probe 函数作为驱动的入口点,智能地判断配置来源(现代的设备树 vs. 老旧的平台数据),并调用相应的创建流程。gpio_led_get_gpiod 是一个关键的辅助函数,它封装了从多种遗留(legacy)和现代方法中获取 GPIO 资源的复杂逻辑,是该驱动向后兼容性的基石。

实现原理分析

此代码是 Linux 驱动开发中处理不同硬件抽象层(板级文件 vs. 设备树)和 API 演进(整数 GPIO vs. gpiod)的优秀范例。

  1. 双重配置来源 (gpio_led_probe):

    • probe 函数的核心是一个 if-else 结构,它实现了对两种不同配置风格的“分叉”处理:
      a. 平台数据路径: if (pdata && pdata->num_leds) 检查是否存在平台数据。如果存在,驱动就进入“传统模式”,遍历 pdata->leds C 语言数组来获取每个 LED 的配置。这种方式常见于没有使用设备树的旧内核或架构。
      b. 设备树路径: else { priv = gpio_leds_create(dev); } 如果不存在平台数据,驱动就进入“现代模式”,调用 gpio_leds_create 函数(在上一段代码中分析过),该函数会去解析 probe 函数所绑定设备的设备树子节点。
    • 优先级: 这种设计明确了平台数据优先于设备树。如果一个设备同时拥有 pdata 和设备树节点,只有 pdata 会被使用。
  2. 向后兼容的 GPIO 获取 (gpio_led_get_gpiod):

    • 这个函数是驱动兼容性的核心。它按照从新到旧的顺序,尝试用三种不同的方法来获取一个 GPIO:
      a. devm_gpiod_get_index_optional: 这是最现代的方法。它尝试根据索引从设备获取一个预先声明的 GPIO 描述符 (gpiod)。这种方式通常与设备树或 ACPI 中的 GPIO 映射配合使用,允许板级配置文件通过描述符而不是全局 GPIO 编号来定义连接。
      b. devm_gpio_request_one: 这是“遗留代码路径”的第一步。它使用 template->gpio 中提供的全局 GPIO 整数编号gpio_is_valid 首先检查这个编号是否合法。然后,devm_gpio_request_one 会请求并配置这个 GPIO。
      c. gpio_to_desc: 在通过整数编号成功请求 GPIO 后,此函数将其转换为现代的 gpio_desc 描述符,从而统一了后续代码的处理方式。
    • 极性处理: if (template->active_low ^ gpiod_is_active_low(gpiod)) 这一行巧妙地处理了 GPIO 极性。它使用异或(XOR)操作来判断 pdata 中期望的极性(template->active_low)与 GPIO 控制器本身(或设备树中)定义的极性(gpiod_is_active_low)是否不一致。如果不一致,它会调用 gpiod_toggle_active_low 来“翻转”驱动对该 GPIO 的逻辑视图,从而确保无论底层物理极性如何,驱动写入 1 总是代表“亮”。
  3. 关机处理 (gpio_led_shutdown):

    • 这个回调函数在系统关机或重启的过程中被调用。
    • 它遍历该驱动管理的所有 LED。
    • if (!(led->cdev.flags & LED_RETAIN_AT_SHUTDOWN)): 它检查每个 LED 是否设置了“关机时保持状态”的标志。这个标志可以通过设备树或平台数据来配置。
    • 如果没有设置该标志,它就调用 gpio_led_set(&led->cdev, LED_OFF) 来明确地将 LED 关闭。
    • 目的: 这确保了在系统断电前,大部分非关键的指示灯会被关闭,这是一种良好的电源管理实践,也可以避免在重启序列中出现状态不明确的指示灯。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
// 定义驱动的 OF (设备树) 匹配表。
static const struct of_device_id of_gpio_leds_match[] = {
{ .compatible = "gpio-leds", }, // 驱动会绑定到 compatible 字符串为 "gpio-leds" 的设备树节点。
{},
};

// 将 OF 匹配表注册到模块,以便内核构建设备驱动数据库。
MODULE_DEVICE_TABLE(of, of_gpio_leds_match);

/**
* @brief gpio_led_get_gpiod - 以向后兼容的方式获取 GPIO 描述符。
* @param dev: 父设备。
* @param idx: GPIO 索引 (用于新的 board file 方式)。
* @param template: 包含旧 GPIO 编号和配置的模板。
* @return struct gpio_desc*: 成功则返回 GPIO 描述符,失败返回 ERR_PTR。
*/
static struct gpio_desc *gpio_led_get_gpiod(struct device *dev, int idx,
const struct gpio_led *template)
{
struct gpio_desc *gpiod;
int ret;

/* 路径1: 尝试通过索引获取 gpiod (现代 board file 风格)。 */
gpiod = devm_gpiod_get_index_optional(dev, NULL, idx, GPIOD_OUT_LOW);
if (IS_ERR(gpiod))
return gpiod;
if (gpiod) {
gpiod_set_consumer_name(gpiod, template->name);
return gpiod;
}

/* 路径2: 遗留代码路径,使用全局 GPIO 整数编号。 */

// 检查 GPIO 编号是否有效。
if (!gpio_is_valid(template->gpio))
return ERR_PTR(-ENOENT);

// 请求并配置该 GPIO。
ret = devm_gpio_request_one(dev, template->gpio, GPIOF_OUT_INIT_LOW,
template->name);
if (ret < 0)
return ERR_PTR(ret);

// 将 GPIO 编号转换为 gpiod 描述符。
gpiod = gpio_to_desc(template->gpio);
if (!gpiod)
return ERR_PTR(-EINVAL);

// 根据模板中的 active_low 标志,调整 gpiod 的逻辑极性。
if (template->active_low ^ gpiod_is_active_low(gpiod))
gpiod_toggle_active_low(gpiod);

return gpiod;
}

/**
* @brief gpio_led_probe - GPIO LED 驱动的 probe 函数。
* @param pdev: 平台设备。
* @return int: 成功返回0,失败返回错误码。
*/
static int gpio_led_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct gpio_led_platform_data *pdata = dev_get_platdata(dev); // 获取平台数据
struct gpio_leds_priv *priv;
int i, ret;

// 检查是否存在平台数据 (pdata)。
if (pdata && pdata->num_leds) {
// 平台数据路径:遍历 pdata 中定义的 LED。
priv = devm_kzalloc(dev, struct_size(priv, leds, pdata->num_leds), GFP_KERNEL);
if (!priv)
return -ENOMEM;

priv->num_leds = pdata->num_leds;
for (i = 0; i < priv->num_leds; i++) {
const struct gpio_led *template = &pdata->leds[i];
struct gpio_led_data *led_dat = &priv->leds[i];

// 获取 GPIO 描述符,优先使用 gpiod,否则调用兼容性函数获取。
if (template->gpiod)
led_dat->gpiod = template->gpiod;
else
led_dat->gpiod =
gpio_led_get_gpiod(dev, i, template);
if (IS_ERR(led_dat->gpiod)) {
dev_info(dev, "Skipping unavailable LED gpio %d (%s)\n",
template->gpio, template->name);
continue;
}

// 调用核心函数创建 LED 设备。
ret = create_gpio_led(template, led_dat, dev, NULL,
pdata->gpio_blink_set);
if (ret < 0)
return ret;
}
} else {
// 设备树路径:调用 gpio_leds_create 来解析设备树子节点。
priv = gpio_leds_create(dev);
if (IS_ERR(priv))
return PTR_ERR(priv);
}

platform_set_drvdata(pdev, priv); // 保存私有数据。

return 0;
}

/**
* @brief gpio_led_shutdown - 驱动的关机回调。
* @param pdev: 平台设备。
*/
static void gpio_led_shutdown(struct platform_device *pdev)
{
struct gpio_leds_priv *priv = platform_get_drvdata(pdev);
int i;

// 遍历所有已创建的 LED。
for (i = 0; i < priv->num_leds; i++) {
struct gpio_led_data *led = &priv->leds[i];

// 如果 LED 没有设置“关机时保持状态”的标志...
if (!(led->cdev.flags & LED_RETAIN_AT_SHUTDOWN))
// ...则将其关闭。
gpio_led_set(&led->cdev, LED_OFF);
}
}

// 定义平台驱动结构体。
static struct platform_driver gpio_led_driver = {
.probe = gpio_led_probe,
.shutdown = gpio_led_shutdown,
.driver = {
.name = "leds-gpio",
.of_match_table = of_gpio_leds_match,
},
};

// 使用模块宏来注册平台驱动。
module_platform_driver(gpio_led_driver);

// 模块元数据
MODULE_AUTHOR("Raphael Assenat <raph@8d.com>, Trent Piepho <tpiepho@freescale.com>");
MODULE_DESCRIPTION("GPIO LED driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:leds-gpio");

GPIO LED 驱动:核心创建与回调逻辑

本代码片段展示了 leds-gpio 驱动的核心实现,包括单个 LED 设备的创建逻辑 (create_gpio_led)、设备树解析 (gpio_leds_create) 以及响应上层 LED 子系统请求的回调函数 (gpio_led_set, gpio_blink_set)。其主要功能是将从硬件抽象层(设备树、GPIO 子系统)获取的信息,精确地翻译和绑定到一个标准的 led_classdev 对象上,并提供实现该对象行为的具体方法

实现原理分析

此代码是 Linux 驱动模型中“翻译”和“实现”的典范。它将声明式的硬件描述(设备树)转化为一个功能性的内核对象。

  1. 设备树驱动的创建逻辑 (gpio_leds_create):

    • 此函数是驱动在“现代”(设备树)模式下的入口。
    • 动态分配: 它首先通过 device_get_child_node_count 计算出设备树父节点下有多少个子节点(即多少个 LED),然后使用 devm_kzallocstruct_size 宏来动态地分配一个足够大的 gpio_leds_priv 结构,该结构包含一个灵活数组成员 leds
    • 子节点遍历: device_for_each_child_node_scoped 是一个安全的宏,用于遍历所有子节点。对于每一个子节点 child
      a. devm_fwnode_gpiod_get: 调用此函数从子节点的 gpios 属性中解析出 GPIO 描述符 (gpiod)。这是最关键的硬件资源获取步骤。
      b. 属性解析: 它读取子节点中的其他标准属性,如 retain-state-suspendedpanic-indicator 等,并将它们填充到一个临时的 gpio_led 模板结构中。
      c. create_gpio_led: 调用核心创建函数来完成该 LED 的实例化和注册。
      d. gpiod_set_consumer_name: 这是一个很好的实践。在 LED 设备被成功注册并获得一个最终的名称(如 “user_led”)后,它将这个名称设置回 gpiodconsumer 字段。这在调试时(例如,在 /sys/kernel/debug/gpio 中)非常有帮助,可以清晰地看到哪个 GPIO 被哪个 LED 所使用。
  2. 核心创建与绑定 (create_gpio_led):

    • 此函数是连接 GPIO 子系统和 LED 子系统的核心桥梁
    • 回调函数绑定:
      • led_dat->can_sleep = gpiod_cansleep(led_dat->gpiod);: 这是一个关键的性能和正确性优化。它预先查询 GPIO 控制器是否需要睡眠(例如,通过 I2C/SPI 总线访问)。
      • 根据查询结果,它将 led_dat->cdev.brightness_set(非阻塞)或 led_dat->cdev.brightness_set_blocking(阻塞)指向驱动内部的 gpio_led_set 函数。这确保了 LED 子系统在调用亮度设置接口时,会使用正确的、不会导致死锁的 GPIO API。
    • 初始状态处理: template->default_state == LEDS_GPIO_DEFSTATE_KEEP 这个逻辑允许设备树指定 LED 的初始状态应保持 GPIO 引脚上电时的状态,而不是强制设为开或关。
    • 注册: devm_led_classdev_register_ext 是最终的注册步骤,它将 led_classdev 对象“发布”到系统中,使其在 sysfs 中可见。
  3. LED 操作实现 (gpio_led_set):

    • 这是用户通过 sysfs 写入 brightness 文件时最终被调用的函数。
    • 翻译: 它将 enum led_brightness(对于 GPIO LED,最大亮度为1)简单地翻译成 0(灭)或 1(亮)的 GPIO 电平。
    • 状态处理: 它包含一个 if (led_dat->blinking) 的检查。如果 LED 之前正处于硬件闪烁状态,再次设置亮度会隐式地停止闪烁。
    • _cansleep 调用: 它根据 create_gpio_led 中预先确定的 can_sleep 标志,选择调用 gpiod_set_value_cansleepgpiod_set_value,从而正确地处理可能需要睡眠的 GPIO 操作。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// 定义驱动的私有数据结构,每个 LED 实例一个。
struct gpio_led_data {
struct led_classdev cdev; // 嵌入标准的 LED 类设备结构。
struct gpio_desc *gpiod; // 指向 GPIO 描述符。
u8 can_sleep; // 标志:GPIO 操作是否可以睡眠。
u8 blinking; // 标志:LED 当前是否处于硬件闪烁模式。
gpio_blink_set_t platform_gpio_blink_set; // 指向平台提供的硬件闪烁函数的指针。
};

// ... (cdev_to_gpio_led_data 辅助函数) ...

/**
* @brief gpio_led_set - 设置 GPIO LED 的亮度 (实际是开关)。
* @param led_cdev: 指向 LED 类设备的指针。
* @param value: 要设置的亮度 (LED_OFF 或其他)。
*/
static void gpio_led_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
struct gpio_led_data *led_dat = cdev_to_gpio_led_data(led_cdev);
int level;

// 将亮度值简单地转换为 GPIO 电平 (0 或 1)。
if (value == LED_OFF)
level = 0;
else
level = 1;

// 如果之前处于硬件闪烁状态,则先停止闪烁。
if (led_dat->blinking) {
led_dat->platform_gpio_blink_set(led_dat->gpiod, level,
NULL, NULL);
led_dat->blinking = 0;
} else {
// 根据 GPIO 是否可以睡眠,调用不同的 GPIO 设置函数。
if (led_dat->can_sleep)
gpiod_set_value_cansleep(led_dat->gpiod, level);
else
gpiod_set_value(led_dat->gpiod, level);
}
}

// ... (gpio_blink_set 等其他回调函数的定义) ...

/**
* @brief create_gpio_led - 根据模板创建一个 GPIO LED 实例。
* @param template: 包含配置信息的 gpio_led 模板。
* @param led_dat: 要被初始化的 gpio_led_data 实例。
* @param parent: 父设备。
* @param fwnode: 固件节点 (用于设备树)。
* @param blink_set:可选的硬件闪烁函数指针。
* @return int: 成功返回0,失败返回错误码。
*/
static int create_gpio_led(const struct gpio_led *template,
struct gpio_led_data *led_dat, struct device *parent,
struct fwnode_handle *fwnode, gpio_blink_set_t blink_set)
{
// ... (变量定义) ...

// 检查 GPIO 是否可以睡眠,并据此选择阻塞或非阻塞的亮度设置回调。
led_dat->can_sleep = gpiod_cansleep(led_dat->gpiod);
if (!led_dat->can_sleep)
led_dat->cdev.brightness_set = gpio_led_set;
else
led_dat->cdev.brightness_set_blocking = gpio_led_set_blocking;

// ... (处理默认状态和各种标志位) ...

// 将 GPIO 引脚配置为输出,并设置初始电平。
ret = gpiod_direction_output(led_dat->gpiod, state);
if (ret < 0)
return ret;

// 根据配置来源 (平台数据或设备树),选择不同的注册函数。
if (template->name) {
// 平台数据路径
led_dat->cdev.name = template->name;
ret = devm_led_classdev_register(parent, &led_dat->cdev);
} else {
// 设备树路径
init_data.fwnode = fwnode;
ret = devm_led_classdev_register_ext(parent, &led_dat->cdev,
&init_data);
}

// ... (pinctrl 处理) ...

return ret;
}

// ... (gpio_leds_priv 结构定义) ...

/**
* @brief gpio_leds_create - 从设备树创建一组 GPIO LED。
* @param dev: 父设备 (即 "gpio-leds" 节点对应的设备)。
* @return struct gpio_leds_priv*: 成功则返回包含所有 LED 数据的私有结构,失败返回 ERR_PTR。
*/
static struct gpio_leds_priv *gpio_leds_create(struct device *dev)
{
// ... (变量定义和内存分配) ...

// 遍历父设备的所有设备树子节点。
device_for_each_child_node_scoped(dev, child) {
struct gpio_led_data *led_dat = &priv->leds[used];
struct gpio_led led = {};

// 从子节点的 "gpios" 属性中获取 GPIO 描述符。
led.gpiod = devm_fwnode_gpiod_get(dev, child, NULL, GPIOD_ASIS,
NULL);
// ... (错误处理) ...

led_dat->gpiod = led.gpiod;

// 解析 "default-state" 等标准 LED 属性。
led.default_state = led_init_default_state_get(child);

// 解析 "retain-state-suspended" 等 bool 属性。
if (fwnode_property_present(child, "retain-state-suspended"))
led.retain_state_suspended = 1;
// ... (其他属性解析) ...

// 调用核心函数创建 LED 实例。
ret = create_gpio_led(&led, led_dat, dev, child, NULL);
if (ret < 0)
return ERR_PTR(ret);

// 在 gpiod 上设置 consumer 名字,便于调试。
gpiod_set_consumer_name(led_dat->gpiod,
led_dat->cdev.dev->kobj.name);
used++;
}
// ... (返回私有结构) ...
}

led_classdev_register_ext / led_classdev_unregister / devm_led_classdev_register_ext / devm_led_classdev_release:LED classdev 的注册、注销与 devm 托管

led_classdev_register_ext:带初始化数据的 LED classdev 注册(命名合成、固件属性合并、设备创建、列表挂接与触发器初始化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
int led_classdev_register_ext(struct device *parent,
struct led_classdev *led_cdev,
struct led_init_data *init_data)
{
char composed_name[LED_MAX_NAME_SIZE]; /**< 由父设备/初始化数据合成的候选名称缓冲区。 */
char final_name[LED_MAX_NAME_SIZE]; /**< 处理名称冲突后的最终名称缓冲区。 */
const char *proposed_name = composed_name; /**< 指向“候选名称”的指针:默认指向 composed_name。 */
int ret;

if (init_data) {
/** init_data->devname_mandatory 用于强制要求提供 devicename,避免生成无法区分的设备名。 */
if (init_data->devname_mandatory && !init_data->devicename) {
dev_err(parent, "Mandatory device name is missing");
return -EINVAL;
}

/** led_compose_name:将 parent 与 init_data(例如 devicename 等)组合成可读且稳定的候选名称。 */
ret = led_compose_name(parent, init_data, composed_name);
if (ret < 0)
return ret;

if (init_data->fwnode) {
/** 从固件节点合并默认触发器:用于在注册后选择默认触发器行为。 */
fwnode_property_read_string(init_data->fwnode,
"linux,default-trigger",
&led_cdev->default_trigger);

/** 固件声明“关机保持”时,置位 LED_RETAIN_AT_SHUTDOWN,影响注销/关机路径是否强制熄灭。 */
if (fwnode_property_present(init_data->fwnode,
"retain-state-shutdown"))
led_cdev->flags |= LED_RETAIN_AT_SHUTDOWN;

/** 固件可覆写最大亮度;若读取失败则保持原值,稍后也会有缺省兜底。 */
fwnode_property_read_u32(init_data->fwnode,
"max-brightness",
&led_cdev->max_brightness);

/** color 为可选属性:用于用户空间或触发器/策略识别灯颜色语义。 */
if (fwnode_property_present(init_data->fwnode, "color"))
fwnode_property_read_u32(init_data->fwnode, "color",
&led_cdev->color);
}
} else {
/** 无 init_data 时,使用驱动直接提供的 led_cdev->name 作为候选名。 */
proposed_name = led_cdev->name;
}

/**
* led_classdev_next_name:
* - ret < 0:错误
* - ret == 0:无冲突,final_name 等于 proposed_name
* - ret > 0:发生冲突并自动改名,final_name 为新名称
*/
ret = led_classdev_next_name(proposed_name, final_name, sizeof(final_name));
if (ret < 0)
return ret;
else if (ret && led_cdev->flags & LED_REJECT_NAME_CONFLICT)
return -EEXIST;
else if (ret)
dev_warn(parent, "Led %s renamed to %s due to name collision\n",
proposed_name, final_name);

/** color 的取值范围检查:超范围不阻断注册,但会提示配置不一致。 */
if (led_cdev->color >= LED_COLOR_ID_MAX)
dev_warn(parent, "LED %s color identifier out of range\n", final_name);

/** led_access 用于保护 led_cdev 的关键字段与初始化序列,避免并发路径观察到半初始化状态。 */
mutex_init(&led_cdev->led_access);
mutex_lock(&led_cdev->led_access);

/**
* device_create_with_groups:
* - 创建设备节点并绑定 led_cdev 作为 drvdata
* - 同时创建 led_cdev->groups 指定的属性组(典型是亮度/触发器等 sysfs 接口)
*/
led_cdev->dev = device_create_with_groups(&leds_class, parent, 0,
led_cdev, led_cdev->groups, "%s", final_name);
if (IS_ERR(led_cdev->dev)) {
mutex_unlock(&led_cdev->led_access);
return PTR_ERR(led_cdev->dev);
}

/** 将设备节点与固件节点关联,便于后续基于固件拓扑进行查询/匹配。 */
if (init_data && init_data->fwnode)
device_set_node(led_cdev->dev, init_data->fwnode);

/** 可选特性:硬件“亮度被外部改变”的上报机制,失败需要回滚已创建的设备节点。 */
if (led_cdev->flags & LED_BRIGHT_HW_CHANGED) {
ret = led_add_brightness_hw_changed(led_cdev);
if (ret) {
device_unregister(led_cdev->dev);
led_cdev->dev = NULL;
mutex_unlock(&led_cdev->led_access);
return ret;
}
}

led_cdev->work_flags = 0;
#ifdef CONFIG_LEDS_TRIGGERS
/** 触发器相关读写锁:保护 trigger 指针与触发器切换过程。 */
init_rwsem(&led_cdev->trigger_lock);
#endif
#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED
led_cdev->brightness_hw_changed = -1;
#endif

/** 将新 LED 加入全局 LEDs 列表:用写锁保护插入,保证遍历一致性。 */
down_write(&leds_list_lock);
list_add_tail(&led_cdev->node, &leds_list);
up_write(&leds_list_lock);

/** 若未设置 max_brightness,则采用 LED 子系统缺省亮度上限。 */
if (!led_cdev->max_brightness)
led_cdev->max_brightness = LED_FULL;

/** 同步 cdev 缓存亮度与硬件/软件状态,避免注册后初始读写不一致。 */
led_update_brightness(led_cdev);

/** 绑定 LED 子系统工作队列:后续异步亮度设置/触发器动作可能依赖该队列。 */
led_cdev->wq = leds_wq;

/** 初始化 LED core 内部结构与工作项等。 */
led_init_core(led_cdev);

#ifdef CONFIG_LEDS_TRIGGERS
/** 若存在默认触发器(来自驱动或固件),在此阶段选择并激活。 */
led_trigger_set_default(led_cdev);
#endif

mutex_unlock(&led_cdev->led_access);

dev_dbg(parent, "Registered led device: %s\n",
led_cdev->name);

return 0;
}
EXPORT_SYMBOL_GPL(led_classdev_register_ext);

led_classdev_unregister:LED classdev 注销(触发器解除、停止闪烁、按策略熄灭、回收设备节点与列表项)

作用与原理(关键点)

  • 若启用触发器,需先在 trigger_lock 保护下解除触发器,避免触发器路径在注销后仍访问该 LED。
  • 设置 LED_UNREGISTERING,并停止软件闪烁;若未设置 LED_RETAIN_AT_SHUTDOWN,则在注销时将 LED 置为灭。
  • flush_work(&set_brightness_work) 关键:保证异步亮度设置工作项不会在对象回收后继续运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void led_classdev_unregister(struct led_classdev *led_cdev)
{
if (IS_ERR_OR_NULL(led_cdev->dev))
return;

#ifdef CONFIG_LEDS_TRIGGERS
down_write(&led_cdev->trigger_lock);
if (led_cdev->trigger)
led_trigger_set(led_cdev, NULL); /**< 解除触发器,避免触发器回调在注销后访问已失效对象。 */
up_write(&led_cdev->trigger_lock);
#endif

led_cdev->flags |= LED_UNREGISTERING; /**< 标记注销中:为并发路径提供状态判定依据。 */

led_stop_software_blink(led_cdev); /**< 停止软件闪烁,避免定时器/工作项继续切换亮度。 */

/** 若未声明关机保持,则在注销阶段主动熄灭,保证设备回收后硬件处于确定态。 */
if (!(led_cdev->flags & LED_RETAIN_AT_SHUTDOWN))
led_set_brightness(led_cdev, LED_OFF);

flush_work(&led_cdev->set_brightness_work); /**< 等待异步亮度设置完成,避免 use-after-free 风险。 */

if (led_cdev->flags & LED_BRIGHT_HW_CHANGED)
led_remove_brightness_hw_changed(led_cdev);

device_unregister(led_cdev->dev); /**< 回收设备节点及其属性组。 */

down_write(&leds_list_lock);
list_del(&led_cdev->node); /**< 从全局 LEDs 列表移除,避免后续遍历访问。 */
up_write(&leds_list_lock);

mutex_destroy(&led_cdev->led_access);
}
EXPORT_SYMBOL_GPL(led_classdev_unregister);

devm_led_classdev_release:devres 释放回调(触发 LED 自动注销)

作用与原理(关键点)

  • 这是 devres 资源项的释放函数:当 parent 设备解绑或资源回收时,自动调用 led_classdev_unregister(),从而把 “注册/注销” 与设备生命周期绑定。
1
2
3
4
static void devm_led_classdev_release(struct device *dev, void *res)
{
led_classdev_unregister(*(struct led_classdev **)res); /**< res 保存的是 led_cdev 指针地址,释放时执行注销。 */
}

devm_led_classdev_register_ext:对 led_classdev_register_ext 的 devm 封装(注册成功后把注销动作挂到 devres)

作用与原理(关键点)

  • devres_alloc() 分配一个资源项,资源项携带 devm_led_classdev_release() 作为析构回调。
  • 若注册失败,释放资源项并返回;若成功,把 led_cdev 指针写入资源项并 devres_add()parent,实现自动注销。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int devm_led_classdev_register_ext(struct device *parent,
struct led_classdev *led_cdev,
struct led_init_data *init_data)
{
struct led_classdev **dr; /**< devres 保存的指针槽位:用于在 release 时拿到 led_cdev。 */
int rc;

dr = devres_alloc(devm_led_classdev_release, sizeof(*dr), GFP_KERNEL);
if (!dr)
return -ENOMEM;

rc = led_classdev_register_ext(parent, led_cdev, init_data);
if (rc) {
devres_free(dr); /**< 注册失败时不应把 release 动作挂到 devres,直接释放资源项。 */
return rc;
}

*dr = led_cdev; /**< 保存被托管对象指针,供 release 回调注销。 */
devres_add(parent, dr);

return 0;
}
EXPORT_SYMBOL_GPL(devm_led_classdev_register_ext);

Linux 内核 GPIO LED 驱动全面解析(drivers/leds/leds-gpio.c)

介绍

leds-gpio 是一个通用驱动:把“由某个 GPIO 输出电平控制亮灭”的 LED 注册为 LED class 设备。这样用户态可以通过 LED 子系统统一接口控制(brightness、trigger 等),而不需要直接操作 GPIO。

它的定位很清晰:开/关型指示灯(通常 max_brightness = 1),用于状态指示、告警、活动灯等。


历史与背景

诞生要解决的问题

  • GPIO 指示灯在各种板卡上都存在,但如果每个平台都写私有接口,会导致:

    • 用户态接口不统一;
    • 无法复用 LED 子系统的 trigger 生态;
    • 多个模块/应用争用 GPIO 时难以管理。
  • leds-gpio 把这类需求统一到 LED 子系统:驱动负责“GPIO ↔ LED classdev”的桥接。

重要演进点(从当前常见主线实现的结构可观察)

  • 从早期的“GPIO number + 平台数据”逐步走向 gpiod 描述符固件节点(DT/ACPI 的 fwnode)

    • 新路径:从 DT/ACPI 子节点获取 GPIO 描述符并创建 LED;
    • 兼容路径:仍支持旧的 platform_data/legacy GPIO 编号方式(主要为了兼容老平台)。

社区活跃度与主流应用

  • 属于主线通用驱动,广泛用于各类设备的状态灯/指示灯;
  • 和 LED core、trigger、设备树绑定配合使用,是“标准做法”之一。

核心原理与设计

核心工作原理

可以按“probe → 创建每盏灯 → 回调控制”的链路理解:

  1. probe:确定数据来源并枚举 LED
  • 如果有 platform_data:按数组创建多个 LED;
  • 否则:从固件节点(DT/ACPI)枚举子节点,每个子节点代表一盏灯。
  1. 单灯创建:获取 GPIO、解析属性、设置初始状态
    典型会做这些事情:
  • 获取 GPIO(优先走 gpiod/fwnode);

  • 解析 default-state(on/off/keep):

    • keep:尽量保持当前硬件电平对应的状态;
    • on/off:按属性设置初始亮度;
  • 配置 GPIO 为输出并输出初值(避免上电后 LED 状态不可控)。

  1. 注册 LED classdev
  • 设定 max_brightness = 1(开/关);

  • 设置亮度回调:

    • 如果该 GPIO 操作可能睡眠(例如经由 I2C 扩展器):使用 brightness_set_blocking
    • 否则使用 brightness_set(非阻塞路径)。
  • 如果有平台提供的硬件闪烁钩子(可选),则接入 blink_set;否则主要依赖 LED trigger 的通用闪烁机制。

  1. 亮度设置路径
  • 用户态写 brightness 或触发器驱动亮度变化时,最终调用驱动的 set 回调;
  • 回调内部用 gpiod_set_value()gpiod_set_value_cansleep() 输出 0/1。
  1. 关机/挂起语义(按绑定属性影响行为)
    常见会支持类似策略(是否支持以你使用的内核版本为准):
  • retain-state-suspended:挂起/恢复时尽量不改变 LED 状态;
  • retain-state-shutdown:关机路径不强制关灯;
  • panic-indicator:允许在 panic 场景作为指示灯使用。

主要优势

  • 统一接口:天然获得 LED 子系统的 brightness/trigger 生态;
  • 硬件描述解耦:通过 DT/ACPI 描述多盏灯,驱动本身通用;
  • 正确处理“GPIO 可能睡眠”:通过 blocking 回调避免在不允许睡眠的上下文误操作;
  • 易维护:通常大量使用 devm 资源管理,probe 失败/卸载路径更干净。

劣势、局限与不适用点

  • 只能开/关:不适合需要多级亮度或呼吸灯曲线的场景;
  • 高精度/高频闪烁不合适:GPIO 翻转受调度与总线影响,严格时序应交给 PWM/专用控制器;
  • 硬件闪烁依赖平台能力:没有硬件 blink 支持时,闪烁主要靠 trigger(软件/通用机制),实时性与功耗表现取决于系统。

使用场景

首选场景(举例)

  • 设备状态灯:电源、运行、告警、网络/存储活动指示;
  • 需要 trigger:如心跳、定时闪烁、磁盘活动等;
  • 多灯板卡:用 DT/ACPI 一次性描述多个 LED。

不推荐场景(原因)

  • 需要平滑调光/多级亮度:更适合 leds-pwm 或 I2C/SPI LED 控制器驱动;
  • 需要严格波形输出:GPIO LED 驱动定位不是做波形发生器。

对比分析

下面按你关心的维度对比:leds-gpio vs leds-pwm vs 用户态直控 GPIO。

  1. 实现方式
  • leds-gpio:LED classdev → GPIO 输出 0/1(开关)。
  • leds-pwm:LED classdev → PWM 占空比(多级亮度)。
  • 用户态直控 GPIO:应用自己申请 GPIO、自己实现策略/闪烁。
  1. 性能开销
  • leds-gpio:单次开关非常轻;触发器闪烁会引入定时/回调调度开销(通常可接受但不适合高频)。
  • leds-pwm:设置 PWM 状态相对更重,但亮度控制能力强,硬件侧更稳定。
  • 用户态直控:频繁控制会有系统调用与上下文切换开销,且策略需要进程常驻。
  1. 资源占用
  • leds-gpio:每盏灯少量内核对象;通常不需要常驻用户进程。
  • leds-pwm:需要 PWM 控制器资源与状态对象。
  • 用户态直控:需要用户进程与其定时器/事件循环资源。
  1. 隔离级别
  • leds-gpio / leds-pwm:由内核 LED 子系统统一对外导出,减少多进程争用与权限扩散。
  • 用户态直控:GPIO 权限与资源仲裁更难,容易出现多个组件同时抢占。
  1. 启动速度
  • leds-gpio:GPIO 就绪即可点灯,通常很早能工作。
  • leds-pwm:取决于 PWM 控制器初始化与时钟资源。
  • 用户态直控:取决于用户进程启动时机,早期阶段不稳定。

总结

关键特性

  • 通用 GPIO 指示灯驱动:把 GPIO LED 纳入 LED 子系统;
  • 典型为开关型(max_brightness=1),通过 gpiod_cansleep 区分阻塞/非阻塞设置路径;
  • 支持 DT/ACPI 描述多灯,并可结合 trigger 做统一的状态指示策略。

gpio_led_probe / gpio_led_shutdown / gpio_led_driver: GPIO LED 平台驱动的探测初始化与关机收尾

gpio_led_probe: 构造 LED 设备实例并绑定到 platform_device

这个函数是 GPIO LED 平台驱动的设备级初始化入口。它根据是否存在 platform_data(传统板级静态配置)走两条路径:

  1. platform_data 路径:按 pdata->num_leds 遍历模板数组,为每个 LED 获取 gpiod(优先复用模板里已给的 gpiod,否则调用 gpio_led_get_gpiod 动态获取),再调用 create_gpio_led 完成 LED classdev 注册与初始状态配置。
  2. 无 platform_data 路径:调用 gpio_leds_create(通常对应设备树/固件描述)创建并初始化 privleds[]

该函数体现的关键技巧是:

  • devm 管理内存devm_kzallocpriv 生命周期绑定到 dev,probe 失败或设备卸载时自动释放,减少错误路径清理复杂度。
  • 可变长度结构体分配struct_size(priv, leds, n) 计算 struct gpio_leds_priv + n 个 leds[] 的总大小,避免手工溢出。
  • 弱失败策略:GPIO 不可用时并不立即失败,而是 continue 跳过该 LED(仅在拿到 GPIO 后创建 LED 失败才返回错误)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/**
* @brief GPIO LED 平台驱动的 probe 回调:创建并注册 LED 实例
*
* @param pdev 平台设备对象,承载 struct device 以及固件描述/平台数据
* @return 成功返回 0;失败返回负错误码
*/
static int gpio_led_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev; /* 当前设备对象,用于 devm 资源管理与日志输出 */
struct gpio_led_platform_data *pdata = dev_get_platdata(dev); /* 板级平台数据(若存在) */
struct gpio_leds_priv *priv; /* 驱动私有数据:保存 LED 数组与数量等 */
int i, ret; /* i 用于遍历 LED,ret 保存返回码 */

/* 若存在 platform_data 且声明了 LED 数量,则按 platform_data 模板创建 */
if (pdata && pdata->num_leds) {
/* 分配包含可变长度 leds[] 的私有结构体,并用 0 初始化 */
priv = devm_kzalloc(dev,
struct_size(priv, leds, pdata->num_leds),
GFP_KERNEL);
if (!priv)
return -ENOMEM; /* 内存不足:无法继续 */

priv->num_leds = pdata->num_leds; /* 保存 LED 数量,用于后续 shutdown/遍历 */

/* 遍历每个 LED 模板,逐个创建对应 LED 实例 */
for (i = 0; i < priv->num_leds; i++) {
const struct gpio_led *template = &pdata->leds[i]; /* 该 LED 的模板配置 */
struct gpio_led_data *led_dat = &priv->leds[i]; /* 该 LED 的运行期数据 */

/*
* 获取 GPIO 描述符:
* - 若模板已提供 gpiod,则直接复用(常见于板级代码预先解析)
* - 否则通过 gpio_led_get_gpiod 从固件/编号等信息获取
*/
if (template->gpiod)
led_dat->gpiod = template->gpiod;
else
led_dat->gpiod = gpio_led_get_gpiod(dev, i, template);

/*
* GPIO 不可用时跳过该 LED:
* 这允许一个驱动实例在部分资源缺失时仍能提供其它 LED 的功能。
*/
if (IS_ERR(led_dat->gpiod)) {
dev_info(dev,
"Skipping unavailable LED gpio %d (%s)\n",
template->gpio, template->name);
continue;
}

/*
* 创建并注册 LED:
* - 该函数通常会初始化 led_classdev,设置亮灭控制回调,并注册到 LED 子系统
* - 最后一个参数 NULL:这里不提供额外的触发器/额外上下文(由 create_gpio_led 定义)
* - pdata->gpio_blink_set:可选的板级闪烁设置回调(若提供则用于硬件/板级闪烁控制策略)
*/
ret = create_gpio_led(template, led_dat, dev, NULL,
pdata->gpio_blink_set);
if (ret < 0)
return ret; /* 创建失败:直接返回错误,避免半初始化不一致 */
}
} else {
/*
* 无 platform_data:走自动创建路径(通常对应设备树/固件描述)。
* gpio_leds_create 内部会分配并初始化 priv/leds[],并完成必要的解析。
*/
priv = gpio_leds_create(dev);
if (IS_ERR(priv))
return PTR_ERR(priv); /* 传递创建失败原因 */
}

/* 将 priv 绑定到 platform_device,供 shutdown/remove 等路径取回 */
platform_set_drvdata(pdev, priv);

return 0; /* probe 成功 */
}

/**
* @brief 平台设备关机回调:按策略关闭 LED
*
* @param pdev 平台设备对象
*/
static void gpio_led_shutdown(struct platform_device *pdev)
{
struct gpio_leds_priv *priv = platform_get_drvdata(pdev); /* 取回 probe 保存的私有数据 */
int i; /* 遍历索引 */

/* 遍历所有 LED,根据标志位决定是否在关机时强制熄灭 */
for (i = 0; i < priv->num_leds; i++) {
struct gpio_led_data *led = &priv->leds[i]; /* 当前 LED 实例 */

/*
* LED_RETAIN_AT_SHUTDOWN:
* - 若置位:表示关机阶段保持 LED 当前状态(例如用于硬件/电源指示)
* - 若未置位:关机时强制关闭,避免系统退出后 GPIO 悬空导致误亮
*/
if (!(led->cdev.flags & LED_RETAIN_AT_SHUTDOWN))
gpio_led_set(&led->cdev, LED_OFF); /* 通过 LED 子系统接口设置为关闭 */
}
}

static const struct of_device_id of_gpio_leds_match[] = {
{ .compatible = "gpio-leds", },
{},
};

MODULE_DEVICE_TABLE(of, of_gpio_leds_match);

/**
* @brief GPIO LED 的 platform_driver 描述对象
*
* - probe:设备匹配成功后的初始化入口
* - shutdown:关机/重启路径的收尾动作
* - of_match_table:设备树匹配表(决定哪些 DT 节点会触发该驱动)
*/
static struct platform_driver gpio_led_driver = {
.probe = gpio_led_probe, /* 设备探测回调 */
.shutdown = gpio_led_shutdown, /* 关机回调 */
.driver = {
.name = "leds-gpio", /* 驱动名称 */
.of_match_table = of_gpio_leds_match, /* 设备树匹配表 */
},
};

/**
* @brief 使用宏生成模块入口/出口,完成 platform_driver 的注册与注销
*
* 该宏会在模块加载/内建初始化时注册 gpio_led_driver,
* 在模块卸载时注销 gpio_led_driver(若允许卸载)。
*/
module_platform_driver(gpio_led_driver);

gpio_leds_create / struct gpio_leds_priv: 从固件子节点批量创建 GPIO LED,并延迟补全 GPIO label

这个函数是无 platform_data 分支下的创建路径(通常对应设备树/ACPI 的固件描述)。它做的事情可以概括为:

  1. 用子节点数量决定最大分配规模:先统计 device_get_child_node_count(dev),按“最多 count 个 LED”一次性分配 gpio_leds_priv + leds[count]
  2. 逐子节点解析并创建 LED:每个子节点对应一个 LED;为它获取 GPIO 描述符、解析默认状态与若干布尔属性,然后调用 create_gpio_led 把 LED 注册进 LED 子系统。
  3. 注册后再设置 GPIO consumer name:因为在获取 gpiod 时 LED 的最终名字还没确定(注册 LED classdev 后才有 kobj.name),所以先以 NULL label 获取 gpiod,等注册完成后用 gpiod_set_consumer_name 写入最终名字。

关键技巧是第 3 点:先获取 GPIO(label 未定),后注册 LED(名字确定),再回填 GPIO label。这能让调试与资源追踪(GPIO consumer 名称)与最终 LED 对象一致。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
* @brief GPIO LED 驱动的私有数据结构
*
* @param num_leds 当前实际创建并有效的 LED 数量
* @param leds 可变长度数组,元素数量由 num_leds 描述
*
* __counted_by(num_leds) 用于让编译器/静态分析工具理解 leds[] 的边界,
* 降低越界访问风险(属于安全注解,不改变运行时语义)。
*/
struct gpio_leds_priv {
int num_leds; /* 实际 LED 数量 */
struct gpio_led_data leds[] __counted_by(num_leds); /* LED 运行期数据数组 */
};

/**
* @brief 基于固件子节点(设备树/ACPI fwnode)创建 GPIO LED 集合
*
* @param dev 设备对象,子节点挂在该设备下
* @return 成功返回 priv 指针;失败返回 ERR_PTR(负错误码)
*/
static struct gpio_leds_priv *gpio_leds_create(struct device *dev)
{
struct gpio_leds_priv *priv; /* 私有数据结构,包含可变长度 leds[] */
int count; /* 子节点数量(最大 LED 数) */
int used; /* 已成功创建的 LED 数 */
int ret; /* 临时返回值 */

/* 统计子节点数量;若没有子节点,说明没有 LED 描述,返回 -ENODEV */
count = device_get_child_node_count(dev);
if (!count)
return ERR_PTR(-ENODEV);

/* 一次性分配 priv + leds[count],并清零初始化 */
priv = devm_kzalloc(dev, struct_size(priv, leds, count), GFP_KERNEL);
if (!priv)
return ERR_PTR(-ENOMEM);

/* 先按“最大可能值”记录 LED 数,后面会改为 used(实际成功数) */
priv->num_leds = count;
used = 0;

/*
* 遍历每个子节点(scoped 变体保证 child 的生命周期在循环体内有效)。
* 每个子节点对应一个 LED 实例。
*/
device_for_each_child_node_scoped(dev, child) {
struct gpio_led_data *led_dat = &priv->leds[used]; /* 当前 LED 的运行期数据槽位 */
struct gpio_led led = {}; /* 临时 LED 模板对象,用于传给 create_gpio_led */

/*
* 从固件子节点获取 GPIO 描述符。
*
* 关键设计点:
* - 第 3 个参数 label 传 NULL:此时 LED 的最终名字尚未确定
* - 注释说明:LED classdev 注册后才知道最终 LED 名称,因此 label 延后设置
*
* GPIOD_ASIS 表示不对 GPIO 的逻辑取反等做额外转换,由固件描述决定极性。
*/
led.gpiod = devm_fwnode_gpiod_get(dev, child, NULL, GPIOD_ASIS, NULL);
if (IS_ERR(led.gpiod)) {
/*
* 获取 GPIO 失败时:
* - dev_err_probe 负责处理 -EPROBE_DEFER 等常见情形并记录日志
* - 直接返回错误:因为此路径是“从固件描述创建”,缺资源通常意味着描述不完整
*/
dev_err_probe(dev, PTR_ERR(led.gpiod),
"Failed to get GPIO '%pfw'\n", child);
return ERR_CAST(led.gpiod);
}

/* 保存 gpiod 到运行期数据中,后续 LED 控制与 label 更新都依赖它 */
led_dat->gpiod = led.gpiod;

/*
* 解析默认状态:
* 该值决定 LED 在注册/初始化时应呈现的初始亮灭状态(由子节点属性决定)。
enum led_default_state led_init_default_state_get(struct fwnode_handle *fwnode)
{
const char *state = NULL;

if (!fwnode_property_read_string(fwnode, "default-state", &state)) {
if (!strcmp(state, "keep"))
return LEDS_DEFSTATE_KEEP;
if (!strcmp(state, "on"))
return LEDS_DEFSTATE_ON;
}

return LEDS_DEFSTATE_OFF;
}
EXPORT_SYMBOL_GPL(led_init_default_state_get);
*/
led.default_state = led_init_default_state_get(child);

/*
* 解析若干布尔属性:
* - retain-state-suspended:挂起时保持状态
* - retain-state-shutdown:关机时保持状态
* - panic-indicator:作为 panic 指示灯
*
* 这些字段会影响 LED classdev flags 与电源管理/关机路径行为。
*/
if (fwnode_property_present(child, "retain-state-suspended"))
led.retain_state_suspended = 1;
if (fwnode_property_present(child, "retain-state-shutdown"))
led.retain_state_shutdown = 1;
if (fwnode_property_present(child, "panic-indicator"))
led.panic_indicator = 1;

/*
* 创建并注册 LED classdev。
*
* 与 platform_data 路径不同,这里把 child 传入:
* - 使 create_gpio_led 能进一步解析该子节点特有属性(如 label、触发器等)
* 最后一个参数为 NULL:此路径不提供额外的 gpio_blink_set 回调。
*/
ret = create_gpio_led(&led, led_dat, dev, child, NULL);
if (ret < 0)
return ERR_PTR(ret);

/*
* 在 LED classdev 注册完成后,LED 的最终名称已确定(kobj.name 可用)。
* 这里把 gpiod 的 consumer name 设置为 LED 名称,便于调试与资源归属追踪。
*/
gpiod_set_consumer_name(led_dat->gpiod,
led_dat->cdev.dev->kobj.name);

/* 成功创建一个 LED,used 自增 */
used++;
}

/* 将 num_leds 修正为实际成功创建的数量 */
priv->num_leds = used;

return priv; /* 返回创建完成的私有数据 */
}

create_gpio_led:实例化并注册一个 GPIO LED(回调选择/默认状态/策略标志/pinctrl 默认态)

作用与实现原理(仅保留关键点)

函数把 template 的配置写入 led_dat->cdev,并根据 GPIO 访问特性选择合适的 LED 回调;随后确定初始电平、设置 LED core 行为标志,配置 GPIO 输出并注册到 LED 子系统;最后在 LED 设备注册完成后选择 pinctrl 默认态(可选)。


create_gpio_led

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* @brief 创建并注册一个基于 GPIO 的 LED 设备实例。
*
* @param template LED 模板配置(名称、默认状态、触发器、策略标志等)。
* @param led_dat LED 运行时数据(包含 led_classdev、GPIO 描述符等)。
* @param parent 父设备,用于 devm 资源托管与设备层级归属。
* @param fwnode 固件节点句柄,用于扩展注册时关联固件属性。
* @param blink_set 平台硬件闪烁设置回调;为 NULL 表示不支持硬件闪烁。
*
* @return 0 表示成功;负 errno 表示失败。
*/
static int create_gpio_led(const struct gpio_led *template,
struct gpio_led_data *led_dat, struct device *parent,
struct fwnode_handle *fwnode, gpio_blink_set_t blink_set)
{
struct led_init_data init_data = {}; /**< 扩展注册参数:用于把 fwnode 交给 LED core 做属性关联/命名等。 */
struct pinctrl *pinctrl; /**< pinctrl 句柄:用于选择默认引脚状态(复用/上下拉等)。 */
int ret, state; /**< ret:错误码;state:GPIO 初始电平(0/1),同时映射为亮度。 */

led_dat->cdev.default_trigger = template->default_trigger;

led_dat->can_sleep = gpiod_cansleep(led_dat->gpiod); /**< 判断 GPIO 操作是否可能睡眠,用于选择 LED 回调类型。 */
if (!led_dat->can_sleep)
led_dat->cdev.brightness_set = gpio_led_set; /**< 非阻塞回调:要求不得睡眠。 */
else
led_dat->cdev.brightness_set_blocking = gpio_led_set_blocking; /**< 阻塞回调:允许睡眠,适配扩展器类 GPIO。 */

led_dat->blinking = 0;
if (blink_set) {
led_dat->platform_gpio_blink_set = blink_set; /**< 保存平台硬件闪烁回调,供 gpio_blink_set() 转调。 */
led_dat->cdev.blink_set = gpio_blink_set; /**< 向 LED core 暴露闪烁控制入口。 */
}

if (template->default_state == LEDS_GPIO_DEFSTATE_KEEP) {
state = gpiod_get_value_cansleep(led_dat->gpiod); /**< KEEP:读取当前电平,避免覆写上电/引导阶段已设定的状态。 */
if (state < 0)
return state;
} else {
state = (template->default_state == LEDS_GPIO_DEFSTATE_ON); /**< ON/OFF:直接映射为 1/0。 */
}

led_dat->cdev.brightness = state;
led_dat->cdev.max_brightness = 1;

if (!template->retain_state_suspended)
led_dat->cdev.flags |= LED_CORE_SUSPENDRESUME; /**< 允许 LED core 在挂起/恢复阶段保存并恢复亮度策略。 */
if (template->panic_indicator)
led_dat->cdev.flags |= LED_PANIC_INDICATOR; /**< 标记为 panic 指示灯:panic 路径可能优先控制它。 */
if (template->retain_state_shutdown)
led_dat->cdev.flags |= LED_RETAIN_AT_SHUTDOWN; /**< 标记关机阶段保持:尽量不在 shutdown 流程改变其状态。 */

ret = gpiod_direction_output(led_dat->gpiod, state); /**< 设置为输出并写入初始电平,确保硬件与 cdev 缓存一致。 */
if (ret < 0)
return ret;

if (template->name) {
led_dat->cdev.name = template->name; /**< 指定 LED 设备名(影响 sysfs 节点与触发器绑定)。 */
ret = devm_led_classdev_register(parent, &led_dat->cdev); /**< devm:设备解绑时自动释放/注销。 */
} else {
init_data.fwnode = fwnode; /**< 无显式名称时,使用 fwnode 让 LED core 关联固件属性。 */
ret = devm_led_classdev_register_ext(parent, &led_dat->cdev,
&init_data);
}

if (ret)
return ret;

pinctrl = devm_pinctrl_get_select_default(led_dat->cdev.dev); /**< 注册后 cdev.dev 才有效,因此 pinctrl 选择放在注册之后。 */
ret = PTR_ERR_OR_ZERO(pinctrl); /**< 统一把 ERR_PTR 转换为 errno,成功则为 0。 */
if (ret == -ENODEV)
ret = 0; /**< 未提供 pinctrl 默认态:视为可选能力缺失,不作为错误。 */
if (ret) {
dev_warn(led_dat->cdev.dev, "Failed to select %pfw pinctrl: %d\n",
fwnode, ret); /**< 其他 pinctrl 错误:提示但不在此处强行修复。 */
}

return ret;
}

gpio_led_set:LED core 的非阻塞亮度设置入口(同时负责终止硬件闪烁态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @brief 设置 GPIO LED 的亮度(非阻塞回调入口)。
*
* @param led_cdev LED core 传入的 classdev 指针。
* @param value 目标亮度;本驱动将其折算为 0/1 电平。
*/
static void gpio_led_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
struct gpio_led_data *led_dat = cdev_to_gpio_led_data(led_cdev); /**< 由 cdev 反查到驱动私有数据,包含 gpiod/can_sleep/blinking 等。 */
int level; /**< GPIO 目标电平:0 表示灭,1 表示亮。 */

/* 将 LED 亮度折算为二值 GPIO 电平 */
if (value == LED_OFF)
level = 0;
else
level = 1;

/* 若当前处于硬件闪烁态,则优先终止/覆盖闪烁,使 LED 回到指定常亮电平 */
if (led_dat->blinking) {
led_dat->platform_gpio_blink_set(led_dat->gpiod, level,
NULL, NULL); /**< 平台回调:以“非闪烁参数”覆盖硬件闪烁,并设置目标电平。 */
led_dat->blinking = 0; /**< 清除状态:后续亮度设置按常规 GPIO 置值路径执行。 */
} else {
/* 根据 GPIO 提供者是否可能睡眠,选择正确的置值接口,避免在不可睡眠上下文调用睡眠型实现 */
if (led_dat->can_sleep)
gpiod_set_value_cansleep(led_dat->gpiod, level); /**< 适配可能睡眠的 GPIO(例如外置扩展器)。 */
else
gpiod_set_value(led_dat->gpiod, level); /**< 适配不可睡眠的 GPIO(STM32 片上 GPIO 常见)。 */
}
}

gpio_led_set_blocking:LED core 的可睡眠亮度设置入口(复用 gpio_led_set)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief 设置 GPIO LED 的亮度(阻塞回调入口,允许睡眠的上下文使用)。
*
* @param led_cdev LED core 传入的 classdev 指针。
* @param value 目标亮度。
*
* @return 固定返回 0,表示该封装层不额外引入错误码。
*/
static int gpio_led_set_blocking(struct led_classdev *led_cdev,
enum led_brightness value)
{
gpio_led_set(led_cdev, value); /**< 复用同一套“终止闪烁/按 can_sleep 选择置值接口”的策略。 */
return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @brief 设置 GPIO LED 的闪烁参数(由 LED core 触发器等机制调用)。
*
* @param led_cdev LED core 传入的 classdev 指针。
* @param delay_on 亮保持时间(单位由 LED core 约定),平台可据此配置硬件闪烁。
* @param delay_off 灭保持时间。
*
* @return 平台硬件闪烁回调返回值;负值表示失败。
*/
static int gpio_blink_set(struct led_classdev *led_cdev,
unsigned long *delay_on, unsigned long *delay_off)
{
struct gpio_led_data *led_dat = cdev_to_gpio_led_data(led_cdev); /**< 获取驱动私有数据以访问平台闪烁回调与 gpiod。 */

led_dat->blinking = 1; /**< 标记进入“硬件闪烁态”:后续亮度设置应先终止/覆盖闪烁。 */
return led_dat->platform_gpio_blink_set(led_dat->gpiod, GPIO_LED_BLINK,
delay_on, delay_off); /**< 平台回调:根据 on/off 周期配置硬件闪烁。 */
}

drivers/leds/trigger/ledtrig-heartbeat.c

led_heartbeat_function / led_invert_show / led_invert_store:心跳触发器的定时相位机与 invert 属性

作用与实现原理

led_heartbeat_function()phase 作为相位状态机,周期性地产生“短亮-短灭-短亮-长灭”的四段式节奏。其 period 通过一个有界的分式函数随 1 分钟负载变化:负载越高,周期越短(心跳越快),且周期存在下界与上界,避免极端负载下出现不可控的过快或过慢。

函数还处理两类全局/异步控制:

  • panic_heartbeats 置位后,立即强制熄灭并停止继续调度(以减少 panic 期间的不确定行为)。
  • LED_BLINK_BRIGHTNESS_CHANGE 位触发“闪烁亮度”更新,将 new_blink_brightness 切换为 blink_brightness

struct heartbeat_trig_data:触发器私有状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int panic_heartbeats; /**< panic 通知后置 1,用于停止心跳定时逻辑并强制灭灯。 */

/**
* @brief 心跳触发器的私有数据。
*
* phase : 相位状态机(0..3),决定本次输出亮/灭以及下一次延时。
* period : 一个完整心跳周期的长度(以 jiffies 表示),会随 1 分钟负载动态调整。
* timer : 周期性定时器,用于驱动相位推进。
* invert : 反相输出开关,为 0 时相位 0/2 亮,为 1 时相位 1/3 亮。
*/
struct heartbeat_trig_data {
struct led_classdev *led_cdev; /**< 关联的 LED 设备。 */
unsigned int phase; /**< 相位状态机变量。 */
unsigned int period; /**< 心跳周期长度(jiffies)。 */
struct timer_list timer; /**< 定时器对象。 */
unsigned int invert; /**< 输出反相控制。 */
};

led_heartbeat_function:定时器回调,推进相位并设置 LED 亮度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* @brief 心跳触发器的定时器回调。
*
* 该回调以 phase 为状态,计算本次输出亮度以及下一次回调延时,然后重设定时器。
* 周期 period 会根据 1 分钟负载 avenrun[0] 动态调整,并换算为 jiffies。
*
* @param t 内核定时器指针,用于反查容器结构 heartbeat_trig_data。
*/
static void led_heartbeat_function(struct timer_list *t)
{
struct heartbeat_trig_data *heartbeat_data =
timer_container_of(heartbeat_data, t, timer); /**< 由 timer 指针反查到私有数据容器。 */
struct led_classdev *led_cdev;
unsigned long brightness = LED_OFF; /**< 默认输出为灭,只有特定相位才置为 blink_brightness。 */
unsigned long delay = 0; /**< 下一次定时器触发的相对延时(jiffies)。 */

led_cdev = heartbeat_data->led_cdev;

/* panic 后停止心跳:强制灭灯并不再继续推进相位 */
if (unlikely(panic_heartbeats)) {
led_set_brightness_nosleep(led_cdev, LED_OFF); /**< panic 路径要求不睡眠,使用 nosleep 版本。 */
return;
}

/* 若 LED core 通知闪烁亮度变更,则在此处完成 blink_brightness 的原子切换 */
if (test_and_clear_bit(LED_BLINK_BRIGHTNESS_CHANGE, &led_cdev->work_flags))
led_cdev->blink_brightness = led_cdev->new_blink_brightness;

/* phase 状态机:产生四段式节奏,并在 phase==0 时刷新 period */
switch (heartbeat_data->phase) {
case 0:
/**
* 根据 1 分钟负载动态计算周期(单位毫秒),再换算为 jiffies:
* - 分式结构保证输出有界:负载趋近无穷时周期趋向某下限;负载为 0 时周期较大。
* - 使用 FSHIFT 与 avenrun[] 的定点格式保持计算精度与性能确定性。
*/
heartbeat_data->period = 300 +
(6720 << FSHIFT) / (5 * avenrun[0] + (7 << FSHIFT)); /**< 负载越高,分母越大,增量越小,周期越短。 */
heartbeat_data->period =
msecs_to_jiffies(heartbeat_data->period); /**< 将毫秒周期换算为 jiffies 以用于 mod_timer。 */

delay = msecs_to_jiffies(70); /**< 第一段“短亮/短灭”的固定时长。 */
heartbeat_data->phase++; /**< 推进到下一相位。 */
if (!heartbeat_data->invert)
brightness = led_cdev->blink_brightness; /**< 非反相:相位 0 输出亮。 */
break;

case 1:
delay = heartbeat_data->period / 4 - msecs_to_jiffies(70); /**< 从周期四分之一中扣除已用的 70ms。 */
heartbeat_data->phase++;
if (heartbeat_data->invert)
brightness = led_cdev->blink_brightness; /**< 反相:相位 1 输出亮。 */
break;

case 2:
delay = msecs_to_jiffies(70);
heartbeat_data->phase++;
if (!heartbeat_data->invert)
brightness = led_cdev->blink_brightness; /**< 非反相:相位 2 输出亮。 */
break;

default:
delay = heartbeat_data->period - heartbeat_data->period / 4 -
msecs_to_jiffies(70); /**< 用剩余时间形成“长停顿”。 */
heartbeat_data->phase = 0; /**< 回到相位 0,下一轮重新计算 period。 */
if (heartbeat_data->invert)
brightness = led_cdev->blink_brightness; /**< 反相:相位 3 输出亮。 */
break;
}

led_set_brightness_nosleep(led_cdev, brightness); /**< 该回调在定时器上下文,要求不睡眠。 */
mod_timer(&heartbeat_data->timer, jiffies + delay); /**< 重设下一次触发时间点。 */
}

led_invert_show / led_invert_store:sysfs 属性 invert 的读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* @brief 读取 invert 属性。
*
* @return 输出当前 invert(0/1),以文本形式返回。
*/
static ssize_t led_invert_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct heartbeat_trig_data *heartbeat_data =
led_trigger_get_drvdata(dev); /**< 从触发器设备节点取回私有数据。 */

return sprintf(buf, "%u\n", heartbeat_data->invert);
}

/**
* @brief 写入 invert 属性。
*
* 将用户输入解析为无符号长整型,并压缩为 0/1,写入 heartbeat_data->invert。
* 该开关会改变相位机在各 phase 下的“亮/灭”选择,但不改变 period 与 delay 的结构。
*/
static ssize_t led_invert_store(struct device *dev,
struct device_attribute *attr, const char *buf, size_t size)
{
struct heartbeat_trig_data *heartbeat_data =
led_trigger_get_drvdata(dev);
unsigned long state; /**< 解析后的输入值,最终会被归一化为 0/1。 */
int ret;

ret = kstrtoul(buf, 0, &state); /**< 将字符串解析为无符号长整型,支持 0 前缀的进制自动识别。 */
if (ret)
return ret;

heartbeat_data->invert = !!state; /**< 归一化为布尔语义,避免非 0 值进入状态机造成歧义。 */

return size;
}

static DEVICE_ATTR(invert, 0644, led_invert_show, led_invert_store);

heartbeat_trig_activate / heartbeat_trig_deactivate / heartbeat_trig_init / heartbeat_trig_exit:触发器生命周期管理与 reboot/panic 联动清理

heartbeat_trig_attrs / heartbeat_trig_groups:导出 invert 属性组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief 心跳触发器导出的属性列表。
*
* 当前仅导出 invert,用于控制心跳输出反相。
*/
static struct attribute *heartbeat_trig_attrs[] = {
&dev_attr_invert.attr, /**< invert 属性节点。 */
NULL /**< 属性数组结束标记。 */
};

/**
* @brief 以组的形式注册属性,供 led_trigger 机制挂载到对应 sysfs 目录。
*/
ATTRIBUTE_GROUPS(heartbeat_trig);

heartbeat_trig_activate:触发器激活(分配私有数据、启动相位机定时器)

作用与原理

  • 为每个绑定该触发器的 LED 分配 heartbeat_trig_data,并通过 led_set_trigger_data() 挂到 led_cdev
  • timer_setup() 建立定时器回调为 led_heartbeat_function()
  • blink_brightness 未设置,则用 max_brightness 作为默认闪烁亮度,保证触发器产生“可见输出”。
  • 直接调用一次 led_heartbeat_function() 等效于“立即启动一次相位推进”,避免等待首个 tick 才开始闪烁。
  • 设置 LED_BLINK_SW 表示进入软件闪烁路径(供 LED core 侧识别与协作)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @brief 激活 heartbeat 触发器。
*
* @param led_cdev 被绑定触发器的 LED 设备。
* @return 0 成功;负 errno 失败。
*/
static int heartbeat_trig_activate(struct led_classdev *led_cdev)
{
struct heartbeat_trig_data *heartbeat_data;

heartbeat_data = kzalloc(sizeof(*heartbeat_data), GFP_KERNEL); /**< 分配并清零私有数据,避免未初始化字段进入相位机逻辑。 */
if (!heartbeat_data)
return -ENOMEM;

led_set_trigger_data(led_cdev, heartbeat_data); /**< 将私有数据与该 LED 绑定,后续 show/store/回调可取回同一对象。 */
heartbeat_data->led_cdev = led_cdev;

timer_setup(&heartbeat_data->timer, led_heartbeat_function, 0); /**< 初始化定时器并绑定回调。 */
heartbeat_data->phase = 0;

if (!led_cdev->blink_brightness)
led_cdev->blink_brightness = led_cdev->max_brightness; /**< 未指定闪烁亮度时,使用最大亮度作为默认输出幅度。 */

led_heartbeat_function(&heartbeat_data->timer); /**< 立即执行一次相位推进并启动后续 mod_timer 链。 */

set_bit(LED_BLINK_SW, &led_cdev->work_flags); /**< 标记为软件闪烁参与者,便于 LED core 协调其他闪烁相关行为。 */

return 0;
}

heartbeat_trig_deactivate:触发器反激活(同步停止定时器、释放私有数据)

作用与原理

  • timer_shutdown_sync() 的关键语义是:确保定时器不再排队且回调不在执行中,从而保证随后 kfree() 不会引发 use-after-free。
  • 清除 LED_BLINK_SW,使 LED core 不再把该 LED 视为软件闪烁控制对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @brief 反激活 heartbeat 触发器。
*
* @param led_cdev 被解绑触发器的 LED 设备。
*/
static void heartbeat_trig_deactivate(struct led_classdev *led_cdev)
{
struct heartbeat_trig_data *heartbeat_data =
led_get_trigger_data(led_cdev); /**< 取回激活时绑定的私有数据。 */

timer_shutdown_sync(&heartbeat_data->timer); /**< 同步关闭定时器,保证回调不再运行,避免释放后被访问。 */
kfree(heartbeat_data); /**< 释放私有数据。 */

clear_bit(LED_BLINK_SW, &led_cdev->work_flags); /**< 清除软件闪烁标记。 */
}

heartbeat_led_trigger:触发器对象(名称、激活/反激活、属性组)

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @brief heartbeat 触发器实例。
*
* name 用于用户态选择触发器时的标识;
* activate/deactivate 决定绑定/解绑时的资源管理策略;
* groups 挂接 sysfs 属性组(invert)。
*/
static struct led_trigger heartbeat_led_trigger = {
.name = "heartbeat",
.activate = heartbeat_trig_activate,
.deactivate = heartbeat_trig_deactivate,
.groups = heartbeat_trig_groups,
};

heartbeat_reboot_notifier / heartbeat_panic_notifier:系统事件回调

作用与原理

  • reboot 回调中注销触发器:在重启路径中尽早解除 LED 触发器与 sysfs/回调关系,降低重启阶段资源不一致风险。
  • panic 回调中仅置位 panic_heartbeats:把“停止心跳、强制灭灯”的策略留给定时器回调执行点统一处理,减少 panic 路径中复杂操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @brief reboot 通知回调:在重启阶段注销 heartbeat 触发器。
*/
static int heartbeat_reboot_notifier(struct notifier_block *nb,
unsigned long code, void *unused)
{
led_trigger_unregister(&heartbeat_led_trigger); /**< 解除触发器注册,避免重启阶段继续被选择/调用。 */
return NOTIFY_DONE;
}

/**
* @brief panic 通知回调:停止心跳输出。
*/
static int heartbeat_panic_notifier(struct notifier_block *nb,
unsigned long code, void *unused)
{
panic_heartbeats = 1; /**< 置位后,定时器回调将强制灭灯并停止继续调度。 */
return NOTIFY_DONE;
}

heartbeat_reboot_nb / heartbeat_panic_nb:notifier_block 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @brief reboot 通知块。
*/
static struct notifier_block heartbeat_reboot_nb = {
.notifier_call = heartbeat_reboot_notifier,
};

/**
* @brief panic 通知块。
*/
static struct notifier_block heartbeat_panic_nb = {
.notifier_call = heartbeat_panic_notifier,
};

heartbeat_trig_init / heartbeat_trig_exit:模块初始化与退出

作用与原理

  • init:注册触发器;若成功,再挂接 panic 通知链与 reboot notifier,确保系统关键事件能触发清理/停止策略。
  • exit:按与 init 相反的顺序注销 notifier 与触发器,避免退出后仍收到通知导致回调访问已卸载代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @brief 模块初始化:注册 heartbeat 触发器并挂接 panic/reboot 通知。
*/
static int __init heartbeat_trig_init(void)
{
int rc = led_trigger_register(&heartbeat_led_trigger); /**< 注册触发器,使其可被 LED 设备选择。 */

if (!rc) {
atomic_notifier_chain_register(&panic_notifier_list,
&heartbeat_panic_nb); /**< 挂接 panic 通知:用于停止心跳。 */
register_reboot_notifier(&heartbeat_reboot_nb); /**< 挂接 reboot 通知:用于重启阶段注销触发器。 */
}
return rc;
}

/**
* @brief 模块退出:注销通知并注销 heartbeat 触发器。
*/
static void __exit heartbeat_trig_exit(void)
{
unregister_reboot_notifier(&heartbeat_reboot_nb); /**< 先注销 reboot 通知,避免退出过程中再触发回调。 */
atomic_notifier_chain_unregister(&panic_notifier_list,
&heartbeat_panic_nb); /**< 再注销 panic 通知。 */
led_trigger_unregister(&heartbeat_led_trigger); /**< 最后注销触发器本体。 */
}

module_init(heartbeat_trig_init);
module_exit(heartbeat_trig_exit);

MODULE_AUTHOR("Atsushi Nemoto <anemo@mba.ocn.ne.jp>");
MODULE_DESCRIPTION("Heartbeat LED trigger");
MODULE_LICENSE("GPL v2");