当前位置:首页>Linux>你的 LED 驱动还不够"Linux" —— LED 子系统与 sysfs 标准接口

你的 LED 驱动还不够"Linux" —— LED 子系统与 sysfs 标准接口

  • 2026-10-11 07:10:49
你的 LED 驱动还不够"Linux" —— LED 子系统与 sysfs 标准接口

上一篇我们把 LED 驱动升级到了 platform 驱动 + 设备树 + GPIO 子系统的工程级水准,/dev/led 自动生成、换板子只改 .dts。但你有没有觉得哪里不对?——别的 Linux 系统上控制 LED 都是 echo 1 > /sys/class/leds/xxx/brightness,我们的驱动却只能 ./ledApp /dev/led 1,还得专门写个 C 程序。 今天,我们用 Linux LED 子系统来终结这个问题。


unsetunset先看现状:我们的 LED 驱动"不够 Linux"unsetunset

回顾上一篇的 LED 控制方式:

# 这是我们现在的做法 —— 需要专门的测试程序./ledApp /dev/led 1    # 亮灯./ledApp /dev/led 0    # 灭灯

同一个 LED,看看"标准 Linux 做法"是什么样的:

# 这是 Linux 的标准做法 —— shell一行搞定,不用写任何 C 代码echo 1 > /sys/class/leds/board_led/brightness    # 亮灯echo 0 > /sys/class/leds/board_led/brightness    # 灭灯# 还能设置心跳闪烁echo heartbeat > /sys/class/leds/board_led/trigger# 还能设置定时闪烁echo timer > /sys/class/leds/board_led/triggerecho 500 > /sys/class/leds/board_led/delay_onecho 500 > /sys/class/leds/board_led/delay_off

差距一目了然:

  • 我们的方式:专用 C 程序 → write(fd, "1", 1) → 驱动 → GPIO
  • 标准方式:echo / cat → sysfs → LED 子系统 → GPIO,shell 脚本直接搞定

标准方式好在哪?

  1. 不用写应用层代码 —— shell 直接操作
  2. 可脚本化 —— if [ $(cat brightness) -eq 1 ]; then ... fi
  3. 自带触发器 —— 心跳灯、定时闪烁、mmc 活动指示,内核自带
  4. 统一接口 —— 不管什么平台的 LED,操作方式完全一样

要实现这个效果,我们要用的就是内核的 LED 子系统。


unsetunsetLED 子系统是什么?unsetunset

LED 子系统(drivers/leds/)是 Linux 内核专门为 LED 控制设计的一套驱动框架。它的核心思想是:把"怎么控制一个 LED"抽象成一组标准操作,对上提供 sysfs 接口,对下调用 GPIO 子系统的 API。

你只需要做一件事:实现 brightness_set 回调函数,告诉内核"亮度变了该怎么做"。 其余的 sysfs 文件创建、权限管理、trigger 调度,LED 子系统全部包办。


unsetunset核心数据结构:led_classdevunsetunset

#include<linux/leds.h>structled_classdev {constchar    *name;              /* LED 名字 → /sys/class/leds/<name>/ */unsignedint   max_brightness;    /* 最大亮度值(通常填 1 表示开关 */constchar    *default_trigger;   /* 默认触发器(NULL 表示不自动触发) *//* 你需要实现的回调 */void (*brightness_set)(struct led_classdev *led_cdev,enum led_brightness brightness);enumled_brightness(*brightness_get)(struct led_classdev *led_cdev);};

几个关键字段:

字段
含义
我们的 LED 填什么
name
LED 的名字,sysfs 目录名
"board_led"→ /sys/class/leds/board_led
max_brightness
最大亮度值
1(只有开和关)PWM 调光填255
default_trigger
默认触发器
NULL(不自动触发,手动控制)
brightness_set核心回调:设置亮度
调用 gpio_set_value()
brightness_get
获取当前亮度
可选,不需要可以不填

unsetunset第一步:实现 brightness_set 回调unsetunset

这是整个驱动里你唯一需要"动脑子"的地方——拿到 LED 子系统传过来的亮度值,把它翻译成 GPIO 电平:

staticvoidboard_led_brightness_set(struct led_classdev *led_cdev,enum led_brightness brightness){/*     * LED 子系统传入 brightness 值,范围 0 ~ max_brightness     * 我们的 LED 不支持调光(max_brightness = 1),所以:     *   brightness == 0  →  灭灯     *   brightness  > 0  →  亮灯     * 通过 container_of 拿到我们的设备结构体,再取出 gpio 编号     */structboard_led_dev *dev =container_of(led_cdev, structboard_led_dev, cdev);if (brightness)        gpio_set_value(dev->gpio, 1);   /* 亮灯 */else        gpio_set_value(dev->gpio, 0);   /* 灭灯 */}

就这几行。对比上一篇 led_write 里的 copy_from_user + 判断 + gpio_set_value 整整 15 行,这里被 LED 子系统精简到了 4 行核心逻辑。

原因:LED 子系统已经帮你做了用户空间到内核空间的数据搬运、合法性校验、并发保护。你只需要关心"亮度变了,GPIO 怎么响应"这一件事。


unsetunset第二步:填充 led_classdev 并注册unsetunset

#include<linux/leds.h>staticstructled_classdevboard_led = {    .name           = "board_led",          /* sysfs 目录名 */    .max_brightness = 1,                    /* 0=灭, 1=亮 */    .brightness_set = board_led_brightness_set,};/* 在 probe 中注册 */ret = led_classdev_register(&pdev->dev, &board_led);if (ret < 0) {    pr_err("led: led_classdev_register failed\n");goto fail_xxx;}

led_classdev_register 做的事情(内核内部):

  1. 在 /sys/class/leds/ 下创建 board_led 目录
  2. 在目录下自动创建 brightness、max_brightness、trigger 等 sysfs 文件
  3. 把读写这些文件的操作和你提供的 brightness_set 回调关联起来
  4. 如果有 default_trigger,激活对应的触发器

注册完,echo 1 > /sys/class/leds/board_led/brightness 就能点灯了。

卸载时注销:

led_classdev_unregister(&board_led);

unsetunset第三步:完整驱动代码unsetunset

把 LED 子系统和上一篇的 platform 驱动框架结合起来,完整的驱动如下:

#include<linux/module.h>#include<linux/platform_device.h>#include<linux/of_gpio.h>#include<linux/gpio.h>#include<linux/leds.h>#define LED_NAME    "board_led"/* ============ 设备私有结构 ============ */structboard_led_dev {int gpio;                          /* GPIO 编号 */structled_classdevcdev;/* LED 子系统核心结构 */};static structboard_led_devled;/* ============ 核心回调:亮度设置 ============ */staticvoidboard_led_brightness_set(struct led_classdev *led_cdev,enum led_brightness brightness){/*     * led_cdev 是容器的一个成员,用 container_of 反推出设备结构体指针     */structboard_led_dev *dev =container_of(led_cdev, structboard_led_dev, cdev);if (brightness)        gpio_set_value(dev->gpio, 1);  /* 亮灯 */else        gpio_set_value(dev->gpio, 0);  /* 灭灯 */}/* ============ probe:设备树匹配后调用 ============ */staticintboard_led_probe(struct platform_device *pdev){int ret;/* 1. 从设备树获取 GPIO */    led.gpio = of_get_named_gpio(pdev->dev.of_node, "led-gpios", 0);if (led.gpio < 0) {        pr_err("led: failed to get led-gpios from dt, ret=%d\n", led.gpio);return led.gpio;    }/* 2. 申请 GPIO */    ret = gpio_request(led.gpio, "led");if (ret) {        pr_err("led: gpio_request failed for gpio %d\n", led.gpio);return ret;    }/* 3. 设为输出,默认低电平(LED 灭) */    gpio_direction_output(led.gpio, 0);/* 4. 填充 led_classdev 并注册 */    led.cdev.name           = "board_led";    led.cdev.max_brightness = 1;              /* 只支持开关 */    led.cdev.brightness_set = board_led_brightness_set;    ret = led_classdev_register(&pdev->dev, &led.cdev);if (ret < 0) {        pr_err("led: led_classdev_register failed\n");goto fail_gpio;    }    pr_info("led: probed, /sys/class/leds/board_led/ ready\n");return 0;fail_gpio:    gpio_free(led.gpio);return ret;}/* ============ remove:与 probe 镜像对称 ============ */staticintboard_led_remove(struct platform_device *pdev){    led_classdev_unregister(&led.cdev);  /* 注销 LED 类设备 */    gpio_set_value(led.gpio, 0);         /* 灭灯 */    gpio_free(led.gpio);                 /* 释放 GPIO */    pr_info("led: removed\n");return 0;}/* ============ 设备树匹配表 ============ */static const structof_device_idboard_led_of_match[] = {    { .compatible = "qian,board-led" },    { }};MODULE_DEVICE_TABLE(of, board_led_of_match);/* ============ platform 驱动 ============ */static structplatform_driverboard_led_driver = {    .driver = {        .name           = "board-led",        .of_match_table = board_led_of_match,    },    .probe  = board_led_probe,    .remove = board_led_remove,};module_platform_driver(board_led_driver);MODULE_LICENSE("GPL");MODULE_AUTHOR("qian");MODULE_DESCRIPTION("Linux LED subsystem driver for onboard LED");

代码导读:几个值得注意的点

1. container_of 宏 —— 结构体嵌入的标准玩法

structboard_led_dev *dev =container_of(led_cdev, structboard_led_dev, cdev);

LED 子系统回调传给你的不是 board_led_dev,而是它内部的 cdev 成员。container_of 通过成员的地址反算出外层结构体的地址。这是 Linux 内核最核心的设计模式之一——你不拥有框架,框架拥有你,但你可以通过嵌入来扩展框架。

2. 没有 file_operations,没有 cdev_add,没有 device_create

和本系列前四篇最大的不同:这次我们不注册字符设备了。 LED 子系统自己会创建 sysfs 文件,应用层通过 sysfs 交互,不需要 /dev/led 设备节点。代码量直接少了 40 行。

3. max_brightness = 1 的含义

这不是随便填的。普通 GPIO 控制的 LED 只能开或关,最大亮度就是 1。如果你用的是 PWM 控制的可调光 LED,max_brightness 应该填 255(或更大),brightness 会传入 0~255 之间的值,你在 brightness_set 里把它转换成 PWM 占空比。


unsetunset设备树unsetunset

跟上篇基本一样,只改 compatible:

#include <dt-bindings/gpio/gpio.h>#include <dt-bindings/pinctrl/rockchip.h>/ {    board_led {        compatible = "qian,board-led";        led-gpios = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;        status = "okay";    };};

GPIO_ACTIVE_HIGH 定义在 <dt-bindings/gpio/gpio.h>,RK_PC0 定义在 <dt-bindings/pinctrl/rockchip.h>。如果你的板级 .dts 已经包含了这些头文件,不需要重复添加。


unsetunsetMakefileunsetunset

完全不变:

KERNEL_DIR := /home/qian/rk3568_linux_sdk/kernelARCH := arm64CROSS_COMPILE := aarch64-none-linux-gnu-obj-m := board_led.oall:$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_DIR) M=$(PWD) modulesclean:$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_DIR) M=$(PWD) clean

unsetunset编译、加载、玩转 sysfsunsetunset

# 1. 交叉编译驱动并推送makeadb push board_led.ko /root/# 2. 加载驱动insmod board_led.ko# 3. 查看 sysfs —— LED 子系统自动创建的"控制面板"ls /sys/class/leds/board_led/# brightness  max_brightness trigger device subsystem uevent # 4. 点灯/灭灯echo 1 > /sys/class/leds/board_led/brightness    # 亮echo 0 > /sys/class/leds/board_led/brightness    # 灭# 5. 查看支持的触发器cat /sys/class/leds/board_led/trigger# 6. 设成心跳灯 —— 内核自带,零代码echo heartbeat > /sys/class/leds/board_led/trigger# LED 开始像心跳一样闪烁,on-off-on-off-pause...# 7. 设成定时器闪烁 —— 可调频率echo timer > /sys/class/leds/board_led/triggerecho 100 > /sys/class/leds/board_led/delay_on     # 亮 100msecho 900 > /sys/class/leds/board_led/delay_off    # 灭 900ms# LED 以 1Hz 频率闪烁,占空比 10%# 8. 切回手动控制echo none > /sys/class/leds/board_led/trigger# 9. 在 shell 脚本中判断 LED 状态if [ $(cat /sys/class/leds/board_led/brightness) -eq 1 ]; thenecho"LED is ON"fi# 10. 卸载rmmod board_led

你现在可以在 shell 脚本里控制 LED 了。 不需要写任何 C 代码,不需要交叉编译应用层程序。这就是 Linux 标准接口的力量。


unsetunset三种 LED 驱动方案对比unsetunset

ioremap 直接寄存器篇
设备树+GPIO 子系统篇
LED 子系统篇(本文)
硬件操作
ioremap + readl/writelgpio_set_value()gpio_set_value()
硬件地址
#define 硬编码
设备树
设备树
驱动框架
裸写 module_init
platform_driverplatform_driver
用户接口
write()
 到 /dev/led
write()
 到 /dev/led
sysfs brightness
设备节点
手动 mknod
自动device_create
LED 子系统自动创建 sysfs
应用层代码
必须写 C 程序
必须写 C 程序
shell 直接 echo
触发器
无
无
内核自带(heartbeat/timer/mmc等)
代码量
~160 行
~170 行
~100 行
跨平台
需重写 C 代码
只改 .dts
只改 .dts
"Linux 味"
⭐
⭐⭐
⭐⭐⭐⭐⭐

代码量从 170 行降到 100 行,不是因为我们偷懒了,而是 LED 子系统和 GPIO 子系统帮我们把大量通用逻辑封装掉了。 这就是 Linux 框架的正确打开方式——越是深入内核框架,你要写的代码越少,但前提是你理解框架帮你做了什么。


unsetunsetLED Trigger:内核自带的功能你不必重写unsetunset

LED 子系统最值的功能其实是 trigger(触发器),它让 LED 的行为完全解耦:

常用触发器一览

触发器
效果
典型用途
none
手动控制(默认)
应用层通过 brightness 控制
heartbeat
心跳闪烁
指示系统存活
timer
定时闪烁
自定义频率闪烁,可配delay_on/delay_off
mmc0
MMC/SD 卡活动指示
类似硬盘灯
cpu0
CPU 使用率指示
某些平台支持
default-on
加载后常亮
电源指示灯
gpio
跟随另一个GPIO状态
信号中继

切换 trigger 不需要改驱动代码,甚至不需要卸载模块:

echo heartbeat > /sys/class/leds/board_led/trigger

一秒变成心跳灯,一行 shell 搞定,零代码。 这就是标准化的力量。


unsetunset一个容易踩的坑:GPIO 编号和 led_classdev 不要混用多个 LEDunsetunset

如果你的板子上有多个 LED,比如一个电源灯、一个状态灯,不要在一个驱动实例里注册两个 led_classdev 共用同一个 GPIO 编号。正确的做法有两种:

方案一:一个 LED 一个设备树节点(推荐)

/ {    power_led {        compatible = "qian,board-led";        led-gpios = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;        status = "okay";    };    status_led {        compatible = "qian,board-led";        led-gpios = <&gpio0 RK_PC1 GPIO_ACTIVE_HIGH>;        status = "okay";    };};

两个设备树节点分别匹配同一个驱动的两次 probe,每次创建独立的 led_classdev。但要注意:当前代码用的是全局变量 static struct board_led_dev led,两个 LED 会互相覆盖。多实例的正确做法是把设备数据放在动态分配的内存里:

staticintboard_led_probe(struct platform_device *pdev){structboard_led_dev *led;/* 动态分配,不用全局变量 */int ret;    led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL);if (!led)return -ENOMEM;    led->gpio = of_get_named_gpio(pdev->dev.of_node, "led-gpios", 0);/* ... 后续代码和之前一样 ... */    platform_set_drvdata(pdev, led);  /* 保存到 platform 设备中 */return 0;}staticintboard_led_remove(struct platform_device *pdev){structboard_led_dev *led = platform_get_drvdata(pdev);    led_classdev_unregister(&led->cdev);    gpio_free(led->gpio);/* devm_kzalloc 分配的内存会自动释放,不用 kfree */return 0;}

方案二:一个设备树节点描述多个 LED(高级用法)

使用 leds 子节点配合 led_classdev 的 of_node 字段,内核的 leds-gpio 驱动就是这样做的。这个方案本站后面会单独开一篇讲,本文先用方案一。


unsetunsetLinux 4.19 / 5.10 / 6.1 版本差异unsetunset

LED 子系统的核心 API(led_classdev_register、led_classdev_unregister)在三个版本中完全一致:

变化点
4.19
5.10
6.1
led_classdev_register
✅ drivers/leds/led-class.c
✅
✅
led_classdev_unregister
✅
✅
✅
devm_led_classdev_register
✅ drivers/leds/led-class.c
✅
✅
LED trigger种类
heartbeat/timer/mmc/default-on
新增 pattern trigger
更丰富
gpio_request
 / gpio_set_value
✅ 兼容
✅ 兼容
⚠️ 推荐迁移到 gpiod_*

本文代码在 4.19 / 5.10 / 6.1 上均可编译运行。 如果要迁移到 6.1,建议将 gpio_request 替换为 devm_gpio_request、gpio_set_value 替换为 gpiod_set_value,但现有 API 仍然兼容。


unsetunset本文你学到了什么unsetunset

  1. LED 子系统的设计理念 —— 把"控制 LED"这件事标准化,对上提供 sysfs 接口,对下封装 GPIO 操作
  2. led_classdev 结构体 —— name、max_brightness、brightness_set、default_trigger 四个关键字段
  3. container_of 宏 —— 从结构体成员地址反算外层结构体地址,内核最核心的设计模式之一
  4. sysfs 控制 LED —— echo 1/0 > brightness、cat trigger、设置 heartbeat/timer
  5. LED Trigger —— 内核自带的触发器让你的 LED 秒变心跳灯、硬盘灯、定时闪烁灯
  6. 多实例的正确写法 —— devm_kzalloc 动态分配设备数据,platform_set_drvdata 跨函数传递

unsetunset接下来unsetunset

LED 子系统为单个 LED 提供了标准接口,但实际项目中你可能会遇到更复杂的需求:

  • 不止一个 LED,而是多个 LED 共享同一种控制逻辑——这时候一个一个注册 led_classdev 太啰嗦,内核有没有更省事的办法?
  • 连 led_classdev_register 都不想写——能不能用设备树直接描述 LED,不用写一行 C 代码?

下一篇,我们引入 leds-gpio 驱动 + MISC 设备框架。leds-gpio 是内核自带的通用 GPIO LED 驱动——只要你的 LED 是 GPIO 控制的,设备树里填几个属性就能用,驱动代码一行都不用写。MISC 设备框架则让你能用不到 10 行代码注册一个"轻量级字符设备",适用于那些"一个 read/write 就够用"的简单场景。


关注「钱途无量嵌入式」,专注 Linux 驱动与 BSP 开发,每周硬核输出。

最新文章

随机文章