[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++;
}
// ... (返回私有结构) ...
}