上一篇我们把 LED 驱动升级到了 platform 驱动 + 设备树 + GPIO 子系统的工程级水准,
/dev/led自动生成、换板子只改.dts。但你有没有觉得哪里不对?——别的 Linux 系统上控制 LED 都是echo 1 > /sys/class/leds/xxx/brightness,我们的驱动却只能./ledApp /dev/led 1,还得专门写个 C 程序。 今天,我们用 Linux LED 子系统来终结这个问题。
回顾上一篇的 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差距一目了然:
write(fd, "1", 1) → 驱动 → GPIOecho / cat → sysfs → LED 子系统 → GPIO,shell 脚本直接搞定标准方式好在哪?
if [ $(cat brightness) -eq 1 ]; then ... fi要实现这个效果,我们要用的就是内核的 LED 子系统。
LED 子系统(drivers/leds/)是 Linux 内核专门为 LED 控制设计的一套驱动框架。它的核心思想是:把"怎么控制一个 LED"抽象成一组标准操作,对上提供 sysfs 接口,对下调用 GPIO 子系统的 API。

你只需要做一件事:实现 brightness_set 回调函数,告诉内核"亮度变了该怎么做"。 其余的 sysfs 文件创建、权限管理、trigger 调度,LED 子系统全部包办。
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);};几个关键字段:
name | "board_led"→ | |
max_brightness | 1(只有开和关)PWM 调光填 | |
default_trigger | NULL(不自动触发,手动控制) | |
brightness_set | 核心回调:设置亮度 | gpio_set_value() |
brightness_get |
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 怎么响应"这一件事。
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 做的事情(内核内部):
/sys/class/leds/ 下创建 board_led 目录brightness、max_brightness、trigger 等 sysfs 文件brightness_set 回调关联起来default_trigger,激活对应的触发器注册完,echo 1 > /sys/class/leds/board_led/brightness 就能点灯了。
卸载时注销:
led_classdev_unregister(&board_led);把 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 占空比。
跟上篇基本一样,只改 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已经包含了这些头文件,不需要重复添加。
完全不变:
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# 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 标准接口的力量。
ioremap + | gpio_set_value() | gpio_set_value() | |
#define 硬编码 | |||
module_init | platform_driver | platform_driver | |
write()/dev/led | write()/dev/led | brightness | |
mknod | 自动device_create | ||
echo | |||
| ~100 行 | |||
.dts | .dts | ||
代码量从 170 行降到 100 行,不是因为我们偷懒了,而是 LED 子系统和 GPIO 子系统帮我们把大量通用逻辑封装掉了。 这就是 Linux 框架的正确打开方式——越是深入内核框架,你要写的代码越少,但前提是你理解框架帮你做了什么。
LED 子系统最值的功能其实是 trigger(触发器),它让 LED 的行为完全解耦:
none | ||
heartbeat | ||
timer | delay_on/delay_off | |
mmc0 | ||
cpu0 | ||
default-on | ||
gpio |
切换 trigger 不需要改驱动代码,甚至不需要卸载模块:
echo heartbeat > /sys/class/leds/board_led/trigger一秒变成心跳灯,一行 shell 搞定,零代码。 这就是标准化的力量。
led_classdev 不要混用多个 LEDunsetunset如果你的板子上有多个 LED,比如一个电源灯、一个状态灯,不要在一个驱动实例里注册两个 led_classdev 共用同一个 GPIO 编号。正确的做法有两种:
/ { 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;}使用 leds 子节点配合 led_classdev 的 of_node 字段,内核的 leds-gpio 驱动就是这样做的。这个方案本站后面会单独开一篇讲,本文先用方案一。
LED 子系统的核心 API(led_classdev_register、led_classdev_unregister)在三个版本中完全一致:
led_classdev_register | drivers/leds/led-class.c | ||
led_classdev_unregister | |||
devm_led_classdev_register | drivers/leds/led-class.c | ||
gpio_requestgpio_set_value | gpiod_* |
本文代码在 4.19 / 5.10 / 6.1 上均可编译运行。 如果要迁移到 6.1,建议将 gpio_request 替换为 devm_gpio_request、gpio_set_value 替换为 gpiod_set_value,但现有 API 仍然兼容。
led_classdev 结构体 —— name、max_brightness、brightness_set、default_trigger 四个关键字段container_of 宏 —— 从结构体成员地址反算外层结构体地址,内核最核心的设计模式之一echo 1/0 > brightness、cat trigger、设置 heartbeat/timerdevm_kzalloc 动态分配设备数据,platform_set_drvdata 跨函数传递LED 子系统为单个 LED 提供了标准接口,但实际项目中你可能会遇到更复杂的需求:
led_classdev 太啰嗦,内核有没有更省事的办法?led_classdev_register 都不想写——能不能用设备树直接描述 LED,不用写一行 C 代码?下一篇,我们引入 leds-gpio 驱动 + MISC 设备框架。leds-gpio 是内核自带的通用 GPIO LED 驱动——只要你的 LED 是 GPIO 控制的,设备树里填几个属性就能用,驱动代码一行都不用写。MISC 设备框架则让你能用不到 10 行代码注册一个"轻量级字符设备",适用于那些"一个 read/write 就够用"的简单场景。
关注「钱途无量嵌入式」,专注 Linux 驱动与 BSP 开发,每周硬核输出。