开发板买回来第一件事做什么?我通常先点个灯。虽然听起来有点土,但Linux驱动入门,LED确实是最友好的一课:代码量小、逻辑直观、出错了对系统也没什么杀伤力。不过真要说把这颗灯点得“正规”,让它接入内核的led子系统、通过设备树配置引脚和触发逻辑,而不是在板子的init脚本里对着/sys/class/gpio直接写电平,这里面的门道就比想象中多了。这篇就围绕RK3568这颗芯片,把led子系统和设备树这套组合拳拆开讲清楚。
我默认你手里有一块RK3568的开发板或者核心板,内核版本建议5.10以上。5.10之后led子系统和gpio-led驱动的接口已经很稳定,网上的各种资料基本都对得上,踩坑概率低。如果你是拿其他平台(比如i.MX、全志、STM32MP1)做对照,思路完全通用,把GPIO控制器和pinctrl节点改成自己平台的就成。
1. 先搞清楚LED子系统和设备树是怎么协作的
1.1 LED子系统到底解决什么问题
最早的内核驱动里,LED就是一个普通的字符设备,驱动自己注册misc设备、自己实现ioctl或者read/write,应用层想控制就得写对应的应用程序,驱动多了之后每个LED驱动的接口都还不一样,非常痛苦。
LED子系统做的事情就是把这个流程标准化。它把所有LED都抽象成一个struct led_classdev,注册进内核后自动在/sys/class/leds/下面生成对应的目录。这样不管底层是GPIO、PWM还是I2C扩展芯片,应用层统一操作brightness、trigger、max_brightness这几个属性就能控制,驱动代码不需要重复造轮子。
这个思路本质上是Linux设备模型里“类(class)”的典型应用。运行一个ls /sys/class/leds/,你能看到系统里所有注册成功的LED,每个子目录就是一个LED设备。比如一个标准的GPIO LED,目录下会有brightness、max_brightness、trigger、device等属性文件。
1.2 设备树在驱动加载流程中的角色
设备树的作用是把“板级硬件信息”从C代码里挪到dts文件里。以前写内核驱动,板子上哪个GPIO接了LED,得在驱动源码里硬编码,或者用板级文件里注册platform_device,换一块板子引脚变了就要重新编译内核。现在只需要改dts,驱动代码一行不用动,重新编译dtb就行。
在led驱动场景,设备树提供的主要信息有几类:
- 这个LED挂在哪个GPIO控制器上、引脚号是多少
- 极性是什么(高电平点亮还是低电平点亮)
- 默认状态(亮、灭、保持)
- 默认触发的trigger(心跳、磁盘活动、网络活动等)
- 可选的
color和function,用于更规范地命名设备
gpio-led驱动在匹配到compatible = "gpio-leds"节点后,会对节点下的每个子节点分别调用devm_gpiod_get_optional等接口去请求GPIO,然后注册成一个led_classdev。整个流程里驱动代码完全不关心你在dts里加了几颗灯,逻辑上遵循“实例化一个子节点,就多一颗灯”的模型。
1.3 为什么说“设备树是驱动的参数表”
你可以把dts理解成给驱动传递参数的“配置文件”。比如同一颗RK3568芯片,A公司做的板子上LED接GPIO0_B3,B公司做的板子上接GPIO3_C2,两边只需要各自的dts里指定不同的引脚,内核里跑的是同一份drivers/leds/leds-gpio.c编译出来的驱动。
这正是设备树设计的核心价值,硬件描述和驱动逻辑解耦。所以搞led驱动,一半功夫在设备树语法上,另外一半才是内核里那几十行代码的理解。很多初学者拿着一个dts文件,看到gpios = <&gpio0 RK_PB3 GPIO_ACTIVE_HIGH>完全不知道在说什么,后面我会把这些语法逐项拆开讲。
2. 动手前的准备工作:RK3568的GPIO怎么数引脚
2.1 RK3568 GPIO bank和pin的编号规则
RK3568的GPIO分为GPIO0到GPIO4,一共5个bank,每个bank下面有A0-A7、B0-B7、C0-C7、D0-D7共32个pin。设备树里引用一个GPIO引脚,通常用宏定义来写,这样可读性更高。
重点说下宏,内核里针对Rockchip平台,在include/dt-bindings/pinctrl/rockchip.h里定义了RK_GPIO0到RK_GPIO4的bank编号,以及RK_PA0到RK_PD7的引脚偏移。组合方式就是RK_PB3表示B组的第3个引脚。
#define RK_GPIO0 0 #define RK_PA0 0 #define RK_PA1 1 // ... #define RK_PB0 8 #define RK_PB1 9 #define RK_PB2 10 #define RK_PB3 11所以gpios = <&gpio0 RK_PB3 GPIO_ACTIVE_HIGH>,就是从设备树里取出gpio0这个GPIO控制器的phandle,加上偏移量11(因为是B3,8+3=11),极性是高电平有效,这样就确定了具体的引脚。
实际换算的时候有个惯用公式:pin全局编号 = bank * 32 + 组内偏移。比如GPIO3_C2就是332 + (28+2) = 114。不过dts里不推荐直接用裸数字,宏定义的可读性和可维护性都好得多。
2.2 确认引脚有没有被其他外设占用
RK3568这种SoC,引脚复用非常复杂,同一个物理引脚可能同时有GPIO、UART、I2C、PWM等多种功能。写LED设备树之前,一定要去查芯片的引脚复用表,确定目标引脚没有在别的节点里被配置成其他功能。
检查方法也很简单,在dts里全局搜索引脚的bank和组号。比如你要用GPIO0_B3,就搜GPIO0_B3或者gpio0里面有没有RK_PB3相关的pinctrl配置。有些引脚默认被U-Boot初始化成了别的复用功能,也会导致内核里gpio request时报错或者电平不受控。
我这边的经验是:先看官方板级dts里该引脚的默认状态,再用cat /sys/kernel/debug/gpio查看运行时GPIO占用情况。如果明明dts里写了LED节点,但/sys/class/leds/下没有对应目录,基本就是GPIO申请失败,十有八九是复用冲突或者这个GPIO已经被别的驱动claim了。
2.3 RK3568的GPIO操作需要背靠pinctrl
要理解的是,GPIO控制器和pinctrl在Rockchip平台上是两个独立但又紧密协作的模块。leds-gpio驱动请求GPIO时,gpiolib底层会通过pinctrl子系统把引脚从复用功能切换到GPIO功能。
如果dts里这个引脚已经被配置成iomux为UART等功能,gpio request阶段就会失败,或者出现“引脚被请求但电平无法控制”的诡异现象。在/sys/kernel/debug/pinctrl/下,pinmux-pins文件里能看到每个引脚的当前复用状态,调试时多看一眼能省不少时间。
实际开发中我习惯先把整块板子的dts通读一遍,尤其关注pinctrl节点的配置。对于纯输出的LED,理论上复用配置为GPIO功能就够,但有些SDK里会刻意在LED节点下加一个pinctrl子节点把引脚强制设为GPIO,这种冗余配置在板级dts里很常见,没必要惊讶。
3. 设备树怎么写:从最小节点到完整配置
3.1 最小的gpio-leds节点
先给一个最精简的示例,点一颗默认熄灭、高电平点亮的灯:
/ { leds: gpio-leds { compatible = "gpio-leds"; status-led { label = "status"; gpios = <&gpio0 RK_PB3 GPIO_ACTIVE_HIGH>; }; }; };把这个节点加到根节点下,重新编译内核(或只编译dtb),启动后就会在/sys/class/leds/下看到status目录。
这里compatible = "gpio-leds"是驱动匹配的关键。内核里drivers/leds/leds-gpio.c的of_match表里就写了这个字符串。gpios属性指定GPIO控制器的phandle、引脚偏移和极性标志。label指定LED名称,如果不写label,驱动的命名会退化到用gpio-leds作为前缀,多颗灯的时候根本分不清谁是谁。
3.2 常用属性逐个拆解
除了最基础的label和gpios,实际项目中经常用到下面几个属性:
| 属性 | 类型 | 作用 | 注意事项 |
|---|---|---|---|
linux,default-trigger | string | 指定默认的触发器 | 值必须是在trigger列表里注册过的,例如heartbeat、mmc0、netdev |
default-state | string | 上电后的默认状态 | 可取on、off、keep,keep会保持bootloader阶段的状态 |
function | u32 | 指定LED功能分类 | 需配合color使用,只用label也行但不够规范 |
color | u32 | 指定LED颜色编号 | 需要includedt-bindings/leds/common.h |
panic-indicator | bool | 是否作为内核panic指示灯 | 设置后可在系统崩溃时强制点亮 |
retain-state-suspended | bool | 系统挂起时是否保持状态 | 有的平台要用,有的用不上 |
max-brightness | u32 | 最大亮度值 | GPIO LED默认就是1,一般不用配 |
特别提醒default-state = "keep"这个值。它表示驱动不去改变当前GPIO的电平状态,适合那些在bootloader阶段已经被点亮、希望内核启动后继续保持不闪烁的场景。但用keep的时候有坑:如果bootloader没初始化这个GPIO,内核读取到的是随机电平,表现就是每次开机LED状态都不一样。
3.3 标准做法:用function和color替代裸字符串label
为了让设备树的描述更标准化,新内核引入了一套基于leds-common功能的绑定方式。在led子系统中,推荐用function和color的组合来描述LED类型,例如:
#include <dt-bindings/leds/common.h> leds { compatible = "gpio-leds"; power-led { function = LED_FUNCTION_POWER; color = <LED_COLOR_ID_GREEN>; gpios = <&gpio0 RK_PB3 GPIO_ACTIVE_HIGH>; default-state = "on"; }; };这种情况下驱动的命名会自动变成green:power。控制台里看/sys/class/leds/目录就是green:power了。很多现代发行版和产品固件都推荐这种写法,因为systemd或自研监控脚本可以统一按颜色和功能来匹配LED,而不是靠记忆一串随意的label字符串。
不过有一类老SDK(比如某些3.x内核的BSP)不支持function/color解析,只认label。写之前确认一下你的内核版本和Documentation/devicetree/bindings/leds目录下的文档,文档里说支持哪些属性就用哪些。5.10以上内核基本都支持。
3.4 一颗不够:多颗LED和多颜色灯珠怎么写
实际项目里很少有只点一颗灯的场景。比如一个设备上常亮电源灯、心跳灯、网络灯,dts里就是在同一个gpio-leds节点下加多个子节点:
leds { compatible = "gpio-leds"; pwr-led { function = LED_FUNCTION_POWER; color = <LED_COLOR_ID_RED>; gpios = <&gpio0 RK_PB3 GPIO_ACTIVE_HIGH>; default-state = "on"; }; he-let-led { function = LED_FUNCTION_HEARTBEAT; color = <LED_COLOR_ID_GREEN>; gpios = <&gpio0 RK_PC0 GPIO_ACTIVE_LOW>; linux,default-trigger = "heartbeat"; }; };注意这里我故意把第二颗灯写成了GPIO_ACTIVE_LOW,实际项目中板子的LED接法千奇百怪,有的高电平亮有的低电平亮。极性和硬件电路必须完全对上,搞反了就会出现“点亮时是灭的,写0时才亮”的尴尬情况。
另一个常见坑是多个LED复用同一个GPIO,dts写起来没问题,编译也通过,但运行时第二个节点的gpio_request会失败,日志会打印GPIO busy相关错误。排查方法就是去dmesg | grep gpio看看有没有request失败记录。
4. 驱动侧是怎么响应设备树配置的
4.1 leds-gpio驱动的核心逻辑
drivers/leds/leds-gpio.c的代码比较直观,核心数据结构是struct gpio_led和struct gpio_led_data。驱动的probe函数从设备树里遍历每一个子节点,解析属性,然后:
static int gpio_led_probe(struct platform_device *pdev) { struct gpio_led_platform_data *pdata; // ... for_each_child_of_node(np, child) { // 解析 gpios / label / default-trigger 等 // devm_gpiod_get_index // devm_led_classdev_register } }请求GPIO用的是devm_gpiod_get_optional这类gpiod API,而不是老的gpio_request。gpiod API返回的是struct gpio_desc,封装了GPIO编号、极性等所有信息,而且和设备树、ACPI都能对接。对于LED这种只需要输出高低电平的场景,拿到desc后调用gpiod_direction_output设置方向即可。
注册LED设备时,驱动会填充struct led_classdev里的字段:
struct led_classdev cdev = { .name = "status", .brightness_set_blocking = gpio_led_set_blocking, .brightness_get = gpio_led_get, };brightness_set_blocking是关键回调,用户空间写brightness属性时,最终会调用到这个函数把电平写到GPIO上。
4.2 brightness_set和brightness_set_blocking怎么选
这是新手非常容易困惑的点。brightness_set要求回调函数里不能有睡眠操作,因为可能在原子上下文(比如中断、持有自旋锁)里被调用;brightness_set_blocking则允许睡眠。gpio直接操作寄存器,本质上是立即性的,用哪个都行。但有时候LED挂的是I2C扩展芯片,读写I2C需要睡眠,那就必须用brightness_set_blocking了。
我见过有同事为了图省事,GPIO LED也选了brightness_set,后面改到I2C LED时发现回调改不动,因为应用层时序行为和trigger的调用路径都变了。所以一开始就按“允许睡眠”的标准来选回调,能省掉后面的麻烦。
4.3 trigger是怎么和LED设备绑定的
trigger模块是led子系统的精髓。比如磁盘读写的时候想让LED闪烁,传统方案是上层程序轮询读IO状态然后控制LED,但在内核里,mmc驱动可以直接调ledtrig_disk_activity(),不管你现在注册的是什么LED,只要有disk-activity触发器关联,就会自动闪烁。
trigger绑定的机制是:每个led_classdev在sysfs下有一个trigger属性文件,里面列出所有当前可用的trigger名称,写名称进去就绑定。内核启动时,drivers/leds/trigger/目录下的ledtrig-heartbeat.c、ledtrig-netdev.c等模块会把自己注册到trigger列表里。
dts里配置linux,default-trigger,就是在驱动注册这个led_classdev的时候,自动调用led_trigger_set_default把trigger挂上。内核中对应关系是:leds-gpio驱动读到该属性后,调用devm_led_classdev_register之前先用led_trigger_register_simple注册一个默认的led_trigger结构,然后led_classdev_register内部会把它关联起来。
4.4 为什么有些trigger在sysfs里看不到
经常有这种情况:/sys/class/leds/xxx/trigger下只有none和heartbeat这少数几个选项,netdev、mmc0这种不出现。
原因主要是对应trigger模块没加载,比如ledtrig-netdev.c编成了模块(CONFIG_LEDS_TRIGGER_NETDEV=m),或者内核配置里根本没选(CONFIG_LEDS_TRIGGER_NETDEV=n)。还有个常见原因是内核把trigger模块编进去了,但对应设备(比如mmc0)还没注册,mmc0这个trigger是在mmc子系统内部动态创建的。
查起来就是三件事:看/sys/kernel/debug/leds/trigger_list或者cat Trigger文件;确认Kconfig里trigger相关选项;搜dmesg有没有trigger注册失败的日志。
5. 编译、烧录和用户空间控制实战
5.1 只改dtb还是重新编译内核
如果内核版本没动,只改dts,只需要重新编译dtb。RK3568平台的编译方式一般是:
make ARCH=arm64 rockchip_linux_defconfig make ARCH=arm64 dtbs生成的dtb通常在arch/arm64/boot/dts/rockchip/下(比如rk3568-evb.dtb)。如果你用的是SDK里的构建脚本,也可能在kernel/目录下直接生成。烧录方式取决于你的板子:U-Boot的resource分区、boot分区里打包dtb,或者用fastboot单独刷。
改完dts烧录后,先确认是否生效。启动时如果有调试串口,可以直接在U-Boot阶段看加载的dtb是否包含了新节点——fdt print /leds这种命令在U-Boot里能用的话最方便。没有串口就只能启动后看/proc/device-tree/leds/目录了。
5.2 用户空间如何控制LED
设备树和驱动都正常的前提下,控制LED变得非常简单:
# 点亮 echo 1 > /sys/class/leds/status/brightness # 熄灭 echo 0 > /sys/class/leds/status/brightness # 查看可用trigger cat /sys/class/leds/status/trigger # 绑定心跳trigger echo heartbeat > /sys/class/leds/status/trigger有些LED支持brightness写0-255的中间值,gpio-led这种纯GPIO的只有0和1两个有效值。max_brightness文件会告诉你上限是1,不要试图写非0的值来实现“半亮”——对GPIO来说,只要非0就是全亮。
实际产品上,这种sysfs操作通常封装在应用层服务里。常见风格是:monitor进程根据业务状态去写brightness,或者直接让内核trigger去完成闪烁,业务自己写定期点亮熄灭的脚本(比如3秒一个周期),简单是简单,但闪烁节奏不好控制,而且应用层一直忙等浪费资源。更规范的做法是内核里写一个小trigger,或者复用timer触发器,通过delay_on/delay_off控制闪烁间隔。
5.3 ledmon和systemd的自动化联动
如果你的板子跑的是带systemd的发行版,可以看看ledmon这个工具,它专门用来把sysfs下的LED和磁盘状态绑定。在服务器场景里,识别磁盘物理位置的定位LED就是靠它驱动的。
对嵌入式设备来说,另一条路是在systemd service里写个简单的循环脚本,监控服务状态然后通过sysfs控制LED,但一定不要在主业务线程里直接操作sysfs文件,读写的开销虽然小,但I/O调度波动时这个动作的延迟可能到几十毫秒,闪烁效果会肉眼可见地不均匀。要么用独立进程用timer触发,要么绑内核trigger,这两种方案我都试过,实测后者更稳。
6. 我把坑都踩了一遍:问题排查与速查表
6.1 /sys/class/leds下面就是没有目录
多数情况是驱动没匹配上或者GPIO请求失败。先确认compatible = "gpio-leds"有没有写错、dts有没有编译进去,再看dmesg | grep -i leds有没有报错信息。
如果dmesg里有gpio_request failed或者invalid GPIO之类的记录,去查GPIO编号是否超出范围或者已经被占用。用cat /sys/kernel/debug/gpio看看系统当前所有GPIO的占用状态,找到被占用的GPIO确认是谁占的,然后调整dts里的引脚。
还有一个很容易忽略的坑:某些SDK中,gpio-leds对应的config会被关掉,比如CONFIG_LEDS_GPIO没打开。这种情况下驱动根本没有编进内核,节点写了也不会有反应。zcat /proc/config.gz | grep LEDS_GPIO查一下,一般比较老的BSP里容易碰上。
6.2 灯能亮,但极性反了
这个最容易判断:写1的时候灯灭,写0的时候灯亮。处理办法有两个方向,改硬件或者改dts。正常情况下改dts就够:把GPIO_ACTIVE_HIGH改成GPIO_ACTIVE_LOW。内核里gpiod_set_value会根据描述符的active_low标志把电平反相,用户空间无感知。
有时候你会遇到一种更隐蔽的情况:写1灯亮,过几秒自己又灭了。排查思路是检查是否有其他驱动也在操作同一个引脚,典型的例如某个SDK里把该引脚同时配置成了某个regulator的enable脚,导致电平被其他子系统强行修改了。单纯从dts层面很难发现,得配合/sys/kernel/debug/gpio的实时状态变化追踪。
6.3 trigger不生效,灯一直常亮
默认trigger配置在dts里,但如果你在sysfs里看到的trigger状态是none或者没有进入预期状态,先手动在sysfs里echo一下对应的trigger试试:
echo heartbeat > /sys/class/leds/status/trigger cat /sys/class/leds/status/trigger手动绑定成功,说明驱动和trigger本身没问题,问题出在dts默认配置。这时检查dts里linux,default-trigger的字符串,和/sys/class/leds/status/trigger里列出的名称逐字对比,尤其注意heartbeat和heartbeat这种看起来一样但实际不存在的值。一个常见的低级错误是SDK里写的是heartbeat,但系统里注册的trigger叫heartbeat,差一个字母就无法匹配。
6.4 系统休眠唤醒后LED不受控制
这类问题常见于gpio-led驱动没处理suspend/resume,或者dts里没配retain-state-suspended。默认情况下,内核suspend时gpiod可能被释放或电平被硬件拉低,唤醒后驱动会重新初始化引脚状态。如果硬件电路上LED需要保持某个状态,得在dts里加retain-state-suspended,并在驱动里实现led_classdev_suspend和led_classdev_resume回调,不实现这两个回调,这个属性其实是不生效的。
另一条经验:优先检查板级原理图上这个LED是不是和某个电源域共用,休眠时电源域掉电,LED电平自然不可控,这属于硬件层面的坑,设备树能做的有限。
6.5 常用排查命令速查表
| 操作 | 命令 |
|---|---|
| 查看注册成功的LED设备 | ls /sys/class/leds/ |
| 查看LED当前亮度和最大值 | cat /sys/class/leds/<name>/brightness |
| 查看可用trigger列表 | cat /sys/class/leds/<name>/trigger |
| 查看GPIO实时状态 | cat /sys/kernel/debug/gpio |
| 查看引脚复用状态 | cat /sys/kernel/debug/pinctrl/pinctrl/pinmux-pins |
| 查看设备树节点是否加载 | ls /proc/device-tree/leds/ |
| 查看内核配置LED选项 | zcat /proc/config.gz | grep LEDS |
7. 从GPIO LED扩展到PWM LED和复杂场景
7.1 为什么有时候GPIO控制不了亮度
GPIO只有高低两个状态,LED亮度只有灭和亮。产品上要呼吸灯效果或者多级亮度,就得用PWM。RK3568上有8路PWM,设备树里用pwm-leds节点:
pwm-leds { compatible = "pwm-leds"; breathe-led { label = "breathe"; pwms = <&pwm2 0 5000000 0>; max-brightness = <255>; linux,default-trigger = "none"; }; };pwms第三个参数是周期,单位是纳秒,5000000就是5ms,对应200Hz,肉眼基本不会看到闪烁。PWM LED的底层驱动在drivers/leds/leds-pwm.c,它会把led_classdev的brightness换算成占空比,写echo 128 > brightness就是约50%亮度。
实际使用中PWM LED和GPIO LED最大的差别是:GPIO LED配置不当时最多就是不亮或常亮,PWM LED配置错了可能整个PWM控制器都被占死,导致其他还用PWM的功能(比如背光)跟着出问题。同一个PWM在dts里只能有一个主设备claim,别在多个节点里引用同一个&pwm2,很多板级bug都是这么来的。
7.2 通过regulator-led做电压指示
比PWM更高级一点的是利用regulator-led框架,把LED和某个电源轨的调节状态绑在一起。这在RK3568这类平台上用得少,大多是平板、机顶盒方案用来做低电量指示。原理是LED brightness的变化对应到一个vreg的电压输出,应用层看到的是一个可以“调到任意亮度”的LED,底层实际是duty cycle控制的电源调整。
我不太建议刚入门就去碰这个,因为涉及regulator框架、pwm和led三者交互,出问题时的排查链路会很长。先把gpio-led跑通,对led子系统有完整认知后再扩展不迟。
7.3 LED trigger在实际产品中的使用建议
产品里我见过两类比较主流的trigger用法:一类是状态型(常亮/熄灭表示开机/关机),另一类是事件型(网络活动闪烁、磁盘活动闪烁)。
状态型直接用default-state+ 应用层在高层次状态变化时写brightness就行。事件型尽量复用内核现成的trigger,netdev需要配置device、link、tx、rx等属性,mmcX是磁盘活动,heartbeat是系统心跳,它们都是内核主动推送事件,不占用应用层资源。
如果你要做一个自定义事件(比如“串口有数据时闪烁”),参考ledtrig-netdev.c的实现方式,在事件发生时调led_trigger_event,这个是标准的做法,比在中断处理函数里直接操作GPIO干净得多。
7.4 多板型适配:一份驱动适配多块硬件
设备树带来的最大好处是驱动代码可以完全通用。我维护的项目里有三块硬件,LED配置各不相同,内核里只有一份leds-gpio驱动,差异全部体现在三个dtsi文件里。换板子,只换dtb,应用程序完全不用动。
这里有个小技巧:把LED节点放到一个专门的dtsi文件里,比如rk3568-leds.dtsi,然后各个板型在主dts里include。这样改动led配置时不会误碰核心的SoC dtsi,review起来也清晰得多,git blame也不会动不动就一大片。
8. 聊聊实操中的个性化经验和调优技巧
内核文档里查得到的属性说明我就不重复了,讲几个不太容易从文档里看出来的实操经验。
第一个是关于default-state和U-Boot配合的问题。很多RK3568板子在U-Boot阶段会通过gpio命令点亮电源指示灯,内核启动后如果dts里写的是default-state = "keep",驱动会读取当前GPIO电平并保持。但这个“保持”是有讲究的,gpiod API在request之后会立即应用active_low的反相处理,如果U-Boot置的是物理高电平而dts里定义的是GPIO_ACTIVE_LOW,内核看到的逻辑值反而是0,表现出来就是板子启动的瞬间灯灭了一下又亮,或者反过来。
解决思路是在U-Boot阶段和dts里统一极性的定义方式,要么都用物理电平视角,要么都用逻辑视角。我自己的规则是:U-Boot阶段设置GPIO时不管active_low还是active_high,全都按物理电平直接写寄存器;而dts里则严格按照电路图定义极性。两边都按这个规则来,“keep”才能在启动过程中不发生肉眼可感知的跳变。
第二个经验是调试时善用dev_dbg和gpio debugfs。gpio-led驱动里本身没多少日志,你把CONFIG_DYNAMIC_DEBUG打开后,可以用echo 'file drivers/leds/leds-gpio.c +p' > /sys/kernel/debug/dynamic_debug/control来查看驱动执行路径。虽然led驱动逻辑简单,但如果它和pinctrl的交互出问题,打开这块的调试信息能看到具体是哪个设备在操作同一个引脚,比瞎猜快得多。
第三个是要重视dts的include层次。RK3568的官方SDK里,dts文件通常有三层:SoC级dtsi、板级dtsi、板级dts。LED节点从职责上讲属于板级描述,写在SoC级dtsi里虽然也能跑,但后续换个板子就容易漏改或者出现莫名其妙的重写覆盖问题。按规范放到板级文件里,改起来也放心。
最后聊一下我自己踩过的一个比较隐蔽的坑:溢出配置导致/proc/device-tree/leds显示正常但/sys/class/leds不出现。那一次我在dts里同时配置了function、color和label,三选一其实就够了,但内核在解析function和color时生成了green:power这个名字,和另一个节点生成的名称重复,后注册的那个LED直接被拒绝了。当时看dmesg只有一条led_classdev_register: failed to add,非常不起眼,但sysfs里就是少了一个目录。从那以后,我对命名规范统一做了约束:所有LED节点要么统一用label,要么统一用function+color,绝不混用。这也是多LED项目的实用经验。
LED这块内容基本讲到这就到头了。从设备树节点的语法到内核led子系统的注册流程,从GPIO极性到trigger绑定,前前后后涉及的东西不算复杂,但串联起来就是一套完整的内核驱动开发路径。你要是照着这篇文章把一块RK3568板子上的LED完整点亮,并搞清楚每个环节为什么这么设计,那后续再去碰按键、碰中断、碰其他字符设备驱动,思路会顺非常多。真到了那一步,你会发现所有Linux驱动的套路都是相似的:设备树描述硬件,驱动匹配设备,子系统统一抽象,用户空间通过sysfs或者其他接口访问。LED只是你走上这条路的第一个台阶。