[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子系统的核心是设备驱动 和触发器 分离的生产者/消费者模型。
LED设备驱动(生产者) :
这是一个知道如何控制一个物理LED 的驱动程序。例如,leds-gpio驱动知道如何通过翻转GPIO电平来点亮/熄灭LED;leds-pca9532驱动知道如何通过I2C总线与一个LED控制器芯片通信。
它会填充并注册一个struct led_classdev实例。这个结构体中最核心的回调函数是.brightness_set(),LED核心层通过调用它来改变LED的亮度(0代表熄灭)。
LED核心层(协调者) :
这是位于drivers/leds/led-class.c等文件中的子系统核心。
它管理所有注册的led_classdev,并在sysfs中为它们创建目录(通常在/sys/class/leds/下)。
它还管理所有注册的led_trigger,并通过sysfs文件trigger让用户可以进行关联。
LED触发器(消费者/事件源) :
这是一个代表系统事件 的模块。例如,ledtrig-disk.c会在块设备层有I/O活动时被通知。
当一个触发器被关联到一个LED设备上,并且其代表的事件发生时,触发器会调用LED核心层的函数(如led_trigger_event())。
最终,LED核心层会调用具体LED设备驱动的.brightness_set()回调,完成对物理LED的控制。
用户空间接口(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 int led_update_brightness (struct led_classdev *led_cdev) { int ret; if (led_cdev->brightness_get) { ret = led_cdev->brightness_get(led_cdev); if (ret < 0 ) return ret; led_cdev->brightness = ret; } return 0 ; } EXPORT_SYMBOL_GPL(led_update_brightness);
LED 亮度设置核心逻辑 此代码片段揭示了Linux LED子系统中负责设置亮度的核心”引擎”。它由两个函数组成, 共同实现了一个健壮、高效且支持异步操作的亮度设置机制。其核心原理是采用”快速路径/慢速路径”(Fast Path/Slow Path)的设计模式: 优先尝试一个快速、非阻塞的操作; 如果该操作不可行(因为它可能需要睡眠), 则将任务安全地委托给一个内核工作队列(workqueue)进行异步处理 。
led_set_brightness_nosleep: 安全的亮度设置入口点这个函数是led_set_brightness的下一层实现, 充当了一个安全的”预处理器”和入口点。它负责在尝试改变硬件状态之前, 对输入值进行清理并检查设备的电源状态。
原理 :
输入值净化 : 它使用min()函数确保请求的亮度值不会超过硬件所能支持的最大亮度。这是一个重要的健壮性措施, 防止向底层驱动传递无效值。
电源状态检查 : 它会检查LED_SUSPENDED标志, 如果设备当前处于挂起(suspend)状态, 它会更新软件中缓存的亮度值, 但会跳过所有硬件操作 。这确保了在设备恢复(resume)后, 可以设置正确的亮度, 同时避免了在设备电源关闭时访问硬件寄存器。
分发 : 在通过所有检查后, 它调用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 void led_set_brightness_nosleep (struct led_cdev *led_cdev, unsigned int value) { led_cdev->brightness = min(value, led_cdev->max_brightness); 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: 快速路径/慢速路径分发器这个函数是亮度设置的核心决策者。它决定是立即完成操作, 还是将其推迟到工作队列中。
原理 :
尝试快速路径 : 它首先调用__led_set_brightness。这个内部函数通常会调用硬件驱动提供的brightness_set_blocking回调, 该回调被保证不会睡眠 。如果硬件操作非常简单(例如, 写入一个内存映射的寄存器), 这个回调就会存在并成功返回0。此时, 任务完成, 函数直接返回。
切换到慢速路径 : 如果__led_set_brightness失败(返回非0), 这意味着硬件驱动的亮度设置操作可能会睡眠(例如, 通过I2C或SPI总线通信)。由于当前上下文可能不允许睡眠(例如, 在中断处理程序或持有自旋锁时), 必须采用异步方式。
准备工作 : 它将要设置的亮度值存入delayed_set_value。
设置信标 : 它使用原子操作(set_bit/clear_bit)来设置work_flags中的标志位。这些标志就像是给即将运行的工作队列任务的”指令”。它为”设置为非零值”和”设置为零(关闭)”这两种情况设置了不同的标志, 因为关闭LED可能需要额外处理(如停止硬件闪烁)。
调度任务 : 最后, 它调用queue_work将set_brightness_work任务放入leds_wq工作队列中。内核的工作队列线程会在稍后一个安全的、允许睡眠的上下文中执行这个任务, 任务会读取work_flags和delayed_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 ; } void led_set_brightness_nopm (struct led_cdev *led_cdev, unsigned int value) { if (!__led_set_brightness(led_cdev, value)) return ; led_cdev->delayed_set_value = value; smp_mb__before_atomic(); if (value) { set_bit(LED_SET_BRIGHTNESS, &led_cdev->work_flags); } else { 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); } 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)时被闪烁逻辑覆盖掉。此函数通过以下方式解决这个问题:
检查闪烁状态 : 它首先使用test_bit(LED_BLINK_SW, ...)原子地检查软件闪烁功能是否处于激活状态。
与闪烁逻辑协作 : 如果正在闪烁:
请求关闭闪烁 : 如果新亮度为0, 它不会直接关闭LED, 而是设置一个LED_BLINK_DISABLE标志, 然后调度工作队列任务。这相当于给正在运行的闪烁任务发送一个”请自行优雅关闭”的信号。这样做可以避免在中断等不允许睡眠的上下文中直接操作定时器。
请求改变闪烁亮度 : 如果新亮度非0, 它也不会直接设置, 而是设置一个LED_BLINK_BRIGHTNESS_CHANGE标志, 并将新亮度值存入new_blink_brightness。闪烁任务在下一次运行时会检查这个标志, 并自动采用新的亮度值作为其”亮”状态的电平。
直接设置 : 如果当前没有在进行软件闪烁, 函数会直接调用led_set_brightness_nosleep, 这将最终调用底层硬件驱动的.brightness_set()回调函数来实际改变硬件状态。
工作队列 : queue_work用于将可能引起睡眠的操作(如修改定时器)从中断上下文(Interrupt Context)推迟到进程上下文(Process Context)中执行, 这是编写健壮驱动程序的关键。
原子操作 : test_bit和set_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 void led_set_brightness (struct led_classdev *led_cdev, unsigned int brightness) { if (test_bit(LED_BLINK_SW, &led_cdev->work_flags)) { 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_stop_software_blink: 安全地终止LED的软件闪烁 此函数是一个核心辅助API, 其唯一的作用是安全、完整地停止由内核定时器驱动的LED软件闪爍功能 。当一个触发器被移除, 或者用户通过sysfs写入亮度0来关闭闪烁时, 就会调用此函数。
它的工作原理是执行一个严格的、有序的清理流程, 以确保将LED的状态从”定时器驱动”模式彻底转换回”手动控制”模式, 同时避免任何竞态条件。
同步删除定时器 : timer_delete_sync(&led_cdev->blink_timer)是此函数最关键的操作。blink_timer是周期性触发LED亮灭状态改变的内核定时器。timer_delete_sync函数会停用这个定时器。_sync后缀至关重要, 它保证了函数会等待定时器的处理函数(handler)执行完毕后才返回(如果它恰好正在运行)。这可以防止在处理函数仍在访问led_cdev数据时, 其他代码已经开始修改这些数据, 从而杜绝了”use-after-free”或数据竞争的风险。
状态复位 : 在确认定时器已完全停止后, 函数将与闪烁相关的状态变量blink_delay_on和blink_delay_off清零。这是一种良好的编程实践, 可以防止在未来重新启用闪烁时, 意外地使用了过时的、无效的延迟值。
清除标志位 : 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 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);
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) { struct device *dev = kobj_to_dev(kobj); struct led_classdev *led_cdev = dev_get_drvdata(dev); void *data; int len; down_read(&triggers_list_lock); down_read(&led_cdev->trigger_lock); len = led_trigger_format(NULL , 0 , led_cdev); data = kvmalloc(len + 1 , GFP_KERNEL); if (!data) { up_read(&led_cdev->trigger_lock); up_read(&triggers_list_lock); return -ENOMEM; } len = led_trigger_format(data, len + 1 , led_cdev); up_read(&led_cdev->trigger_lock); up_read(&triggers_list_lock); len = memory_read_from_buffer(buf, count, &pos, data, len); kvfree(data); return len; } 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 static inline bool trigger_relevant (struct led_classdev *led_cdev, struct led_trigger *trig) { 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 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) { struct device *dev = kobj_to_dev(kobj); struct led_classdev *led_cdev = dev_get_drvdata(dev); struct led_trigger *trig ; int ret = count; mutex_lock(&led_cdev->led_access); if (led_sysfs_is_disabled(led_cdev)) { ret = -EBUSY; goto unlock; } if (sysfs_streq(buf, "none" )) { led_trigger_remove(led_cdev); goto unlock; } if (sysfs_streq(buf, "default" )) { led_trigger_set_default(led_cdev); goto unlock; } down_read(&triggers_list_lock); list_for_each_entry(trig, &trigger_list, next_trig) { if (sysfs_streq(buf, trig->name) && trigger_relevant(led_cdev, trig)) { down_write(&led_cdev->trigger_lock); 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; } EXPORT_SYMBOL_GPL(led_trigger_write);
这个辅助函数负责构建用户最终看到的字符串。
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 static int led_trigger_format (char *buf, size_t size, struct led_classdev *led_cdev) { struct led_trigger *trig ; int len = led_trigger_snprintf(buf, size, "%s" , led_cdev->trigger ? "none" : "[none]" ); if (led_cdev->default_trigger) len += led_trigger_snprintf(buf + len, size - len, " default" ); list_for_each_entry(trig, &trigger_list, next_trig) { bool hit; if (!trigger_relevant(led_cdev, trig)) continue ; hit = led_cdev->trigger && !strcmp (led_cdev->trigger->name, trig->name); 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 ) 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 ) i = vsnprintf(NULL , 0 , fmt, args); else 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), 此函数会执行一系列严格的清理步骤:
从列表中移除 : 使用RCU(Read-Copy-Update)安全地将LED设备从旧触发器的控制列表中移除。synchronize_rcu()确保了在继续之前, 所有可能正在读取该列表的代码都已经完成。
取消挂起工作 : cancel_work_sync()确保任何由用户空间写入brightness而排队的异步工作被取消, 防止旧的状态改变干扰新触发器。
停止软件闪烁 : 如果LED正由定时器控制闪烁, 停止它。
移除sysfs属性 : 如果旧触发器添加了自己特有的sysfs文件(例如, “timer”触发器的delay_on/delay_off), 则将它们移除。
调用deactivate回调 : 调用旧触发器自身的deactivate()函数, 让触发器有机会释放它所占用的资源。
状态复位 : 将LED的状态完全重置: trigger指针设为NULL, 关闭LED。
原理 - 激活(Setup)阶段 : 如果传入的新trig不为NULL, 则执行激活步骤:
添加到列表 : 使用RCU将LED设备添加到新触发器的控制列表中。
调用activate回调 : 调用新触发器的activate()函数, 这是将LED控制权正式移交的关键一步。触发器从此开始根据其内部逻辑控制LED。
添加sysfs属性 : 如果新触发器有特有的sysfs文件, 将它们添加到此LED的sysfs目录中。
健壮的错误处理 : 如果激活或添加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 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); if (led_cdev->trigger) { 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_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); } if (trig) { 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(); flush_work(&led_cdev->set_brightness_work); if (trig->activate) ret = trig->activate(led_cdev); 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) { 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_remove 和 led_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 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 ; } void led_trigger_set_default (struct led_classdev *led_cdev) { 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 ; } down_read(&triggers_list_lock); down_write(&led_cdev->trigger_lock); list_for_each_entry(trig, &trigger_list, next_trig) { 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设备驱动模型中的分层和抽象思想,并巧妙地利用内核机制来处理并发和休眠上下文问题。
异步操作与工作队列 (Work Queue) : led_blink_set_nosleep 和 led_set_brightness 函数都可能需要执行可能休眠的操作(例如,调用一个底层驱动提供的brightness_set_blocking函数,或同步删除一个定时器timer_delete_sync)。为了让这些API可以安全地从中断等原子上下文中调用,它们采用了一种延迟执行 的策略。它们并不直接执行操作,而是设置一个标志位(work_flags),然后将一个预先准备好的工作项(set_brightness_work)添加到工作队列中。内核的工作队列线程稍后会在一个可以安全休眠的进程上下文中执行实际的工作。
软件闪烁 (Software Blink) : 对于没有硬件闪烁功能的LED,内核通过一个高精度定时器(blink_timer)来模拟闪烁。led_stop_software_blink 函数负责停止这种模拟。led_set_brightness 在软件闪烁激活时,不会立即改变亮度,而是更新一个目标亮度值,并设置标志位,由定时器的下一次回调函数来应用新的亮度,从而避免与定时器的闪烁逻辑产生冲突。
触发器 (Triggers) : led_trigger_event 函数实现了LED触发器机制。触发器是内核中定义的事件源(如磁盘活动、CPU负载、网络流量等)。一个或多个LED设备可以注册到同一个触发器上。当led_trigger_event被调用时,它会遍历所有注册到该触发器的LED设备,并统一设置它们的亮度。
RCU (Read-Copy-Update) : led_trigger_event 在遍历与触发器关联的LED设备列表时,使用了RCU机制(rcu_read_lock/unlock 和 list_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 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); 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); void led_set_brightness (struct led_classdev *led_cdev, unsigned int brightness) { if (test_bit(LED_BLINK_SW, &led_cdev->work_flags)) { 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 void led_trigger_event (struct led_trigger *trig, enum led_brightness brightness) { struct led_classdev *led_cdev ; if (!trig) return ; trig->brightness = brightness; rcu_read_lock(); list_for_each_entry_rcu(led_cdev, &trig->led_cdevs, trig_list) led_set_brightness(led_cdev, brightness); 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 static ssize_t brightness_show (struct device *dev, struct device_attribute *attr, char *buf) { struct led_classdev *led_cdev = dev_get_drvdata(dev); unsigned int brightness; mutex_lock(&led_cdev->led_access); led_update_brightness(led_cdev); brightness = led_cdev->brightness; mutex_unlock(&led_cdev->led_access); return sprintf (buf, "%u\n" , brightness); } 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); if (led_sysfs_is_disabled(led_cdev)) { ret = -EBUSY; goto unlock; } ret = kstrtoul(buf, 10 , &state); if (ret) goto unlock; if (state == LED_OFF) led_trigger_remove(led_cdev); led_set_brightness(led_cdev, state); ret = size; unlock: mutex_unlock(&led_cdev->led_access); return ret; } 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 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 = led_cdev->max_brightness; mutex_unlock(&led_cdev->led_access); return sprintf (buf, "%u\n" , max_brightness); } 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 #ifdef CONFIG_LEDS_TRIGGERS 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 , }; static const struct attribute_group led_trigger_group = { .bin_attrs = led_trigger_bin_attrs, }; #endif static struct attribute *led_class_attrs [] = { &dev_attr_brightness.attr, &dev_attr_max_brightness.attr, NULL , }; static const struct attribute_group led_group = { .attrs = led_class_attrs, }; static const struct attribute_group *led_groups [] = { &led_group, #ifdef CONFIG_LEDS_TRIGGERS &led_trigger_group, #endif NULL , };
LED类定义及模块安全交互 此代码片段定义了Linux内核中”leds”设备类的核心结构, 并提供了一个关键的辅助函数, 用于在与具体LED设备交互时, 安全地管理其父驱动模块的生命周期。
leds_class : 这是LED子系统的核心。它是一个struct class实例, 负责在sysfs中创建/sys/class/leds目录。它将所有LED设备驱动注册的设备聚合在一起, 并通过.dev_groups为每个设备提供一套标准的sysfs属性(如brightness, trigger等)。它还通过.pm指针关联了电源管理操作, 使得内核的电源管理核心可以统一地对所有LED设备进行挂起(suspend)和恢复(resume)操作。
SIMPLE_DEV_PM_OPS : 这是一个辅助宏, 用于快速定义一个简单的dev_pm_ops结构体。在这里, 它创建了leds_class_dev_pm_ops实例, 并将其中的.suspend和.resume回调分别指向了led_suspend和led_resume函数。这是一种简化驱动代码的常用技巧。
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 static SIMPLE_DEV_PM_OPS (leds_class_dev_pm_ops, led_suspend, led_resume) ;static struct led_classdev *led_module_get (struct device *led_dev) { struct led_classdev *led_cdev ; if (!led_dev) return ERR_PTR(-EPROBE_DEFER); led_cdev = dev_get_drvdata(led_dev); if (!try_module_get(led_cdev->dev->parent->driver->owner)) { put_device(led_cdev->dev); return ERR_PTR(-ENODEV); } return led_cdev; } static const struct class leds_class = { .name = "leds" , .dev_groups = led_groups, .pm = &leds_class_dev_pm_ops, };
LED子系统初始化与资源管理 此代码片段展示了Linux内核中LED类(Class)子系统的核心初始化、退出以及设备资源管理(devm)的相关实现。其核心原理是建立一个标准化的框架, 包括一个专用的工作队列和一个设备类, 使得所有不同类型的LED设备驱动都能以统一的方式注册到内核, 并向用户空间提供一致的控制接口 。
leds_init 和 leds_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 static int __init leds_init (void ) { leds_wq = alloc_ordered_workqueue("leds" , 0 ); if (!leds_wq) { pr_err("Failed to create LEDs ordered workqueue\n" ); return -ENOMEM; } return class_register(&leds_class); } static void __exit leds_exit (void ) { class_unregister(&leds_class); destroy_workqueue(leds_wq); } subsys_initcall(leds_init); module_exit(leds_exit);
devm_led_classdev_unregister 和 devm_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 static int devm_led_classdev_match (struct device *dev, void *res, void *data) { struct led_classdev **p = res; if (WARN_ON(!p || !*p)) return 0 ; return *p == data; } void devm_led_classdev_unregister (struct device *dev, struct led_classdev *led_cdev) { 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("John Lenz, Richard Purdie" ); MODULE_LICENSE("GPL" ); 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)的优秀范例。
双重配置来源 (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 会被使用。
向后兼容的 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 总是代表“亮”。
关机处理 (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 static const struct of_device_id of_gpio_leds_match [] = { { .compatible = "gpio-leds" , }, {}, }; MODULE_DEVICE_TABLE(of, of_gpio_leds_match); static struct gpio_desc *gpio_led_get_gpiod (struct device *dev, int idx, const struct gpio_led *template) { struct gpio_desc *gpiod ; int ret; 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; } if (!gpio_is_valid(template->gpio)) return ERR_PTR(-ENOENT); ret = devm_gpio_request_one(dev, template->gpio, GPIOF_OUT_INIT_LOW, template->name); if (ret < 0 ) return ERR_PTR(ret); gpiod = gpio_to_desc(template->gpio); if (!gpiod) return ERR_PTR(-EINVAL); if (template->active_low ^ gpiod_is_active_low(gpiod)) gpiod_toggle_active_low(gpiod); return gpiod; } 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; if (pdata && pdata->num_leds) { 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]; 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 ; } ret = create_gpio_led(template, led_dat, dev, NULL , pdata->gpio_blink_set); if (ret < 0 ) return ret; } } else { priv = gpio_leds_create(dev); if (IS_ERR(priv)) return PTR_ERR(priv); } platform_set_drvdata(pdev, priv); return 0 ; } static void gpio_led_shutdown (struct platform_device *pdev) { struct gpio_leds_priv *priv = platform_get_drvdata(pdev); int i; for (i = 0 ; i < priv->num_leds; i++) { struct gpio_led_data *led = &priv->leds[i]; 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 驱动模型中“翻译”和“实现”的典范。它将声明式的硬件描述(设备树)转化为一个功能性的内核对象。
设备树驱动的创建逻辑 (gpio_leds_create) :
此函数是驱动在“现代”(设备树)模式下的入口。
动态分配 : 它首先通过 device_get_child_node_count 计算出设备树父节点下有多少个子节点(即多少个 LED),然后使用 devm_kzalloc 和 struct_size 宏来动态地分配一个足够大的 gpio_leds_priv 结构,该结构包含一个灵活数组成员 leds。
子节点遍历 : device_for_each_child_node_scoped 是一个安全的宏,用于遍历所有子节点。对于每一个子节点 child: a. devm_fwnode_gpiod_get: 调用此函数从子节点的 gpios 属性中解析出 GPIO 描述符 (gpiod)。这是最关键的硬件资源获取步骤。 b. 属性解析 : 它读取子节点中的其他标准属性,如 retain-state-suspended、panic-indicator 等,并将它们填充到一个临时的 gpio_led 模板结构中。 c. create_gpio_led: 调用核心创建函数来完成该 LED 的实例化和注册。 d. gpiod_set_consumer_name: 这是一个很好的实践。在 LED 设备被成功注册并获得一个最终的名称(如 “user_led”)后,它将这个名称设置回 gpiod 的 consumer 字段。这在调试时(例如,在 /sys/kernel/debug/gpio 中)非常有帮助,可以清晰地看到哪个 GPIO 被哪个 LED 所使用。
核心创建与绑定 (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 中可见。
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_cansleep 或 gpiod_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 struct gpio_led_data { struct led_classdev cdev ; struct gpio_desc *gpiod ; u8 can_sleep; u8 blinking; gpio_blink_set_t platform_gpio_blink_set; }; 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; 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 { if (led_dat->can_sleep) gpiod_set_value_cansleep(led_dat->gpiod, level); else gpiod_set_value(led_dat->gpiod, level); } } 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) { 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; 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); } return ret; } 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 = {}; led.gpiod = devm_fwnode_gpiod_get(dev, child, NULL , GPIOD_ASIS, NULL ); led_dat->gpiod = led.gpiod; led.default_state = led_init_default_state_get(child); if (fwnode_property_present(child, "retain-state-suspended" )) led.retain_state_suspended = 1 ; ret = create_gpio_led(&led, led_dat, dev, child, NULL ); if (ret < 0 ) return ERR_PTR(ret); gpiod_set_consumer_name(led_dat->gpiod, led_dat->cdev.dev->kobj.name); used++; } }