第四节:编写第一个Overlay——点亮一个GPIO LED,手动加载与卸载

好,咱们正式开始动手了。

前面几节我们把Overlay的概念、编译环境、加载工具都捋了一遍。说实话,光看理论容易犯困。我当年学Overlay的时候,也是看了三天文档,感觉什么都懂了,一上手就懵。后来我总结了一个经验:先让一个LED亮起来,比什么都管用

这一节,我们就写一个最简单的Overlay——控制一个GPIO引脚,点亮一个LED。然后手动加载它,再卸载它。整个过程你亲手做一遍,Overlay的加载机制、生命周期、设备匹配逻辑,就全通了。

4.1 硬件准备与引脚确认

先说说硬件。我假设你手头有一块带GPIO的Linux开发板,比如BeagleBone Black、Raspberry Pi,或者任何跑主线内核的ARM板。LED接在某个GPIO上,正极通过一个电阻(330Ω左右)连到GPIO,负极接地。

我个人习惯先把GPIO的物理引脚号确认清楚。以BeagleBone Black为例,P8.13引脚对应的GPIO是GPIO0_23,也就是GPIO编号23。你可以在芯片手册里查,也可以在板子的设备树里搜。

嗯,这里要注意:不同板子的GPIO编号规则可能不一样。有些用gpiochip的偏移量,有些直接给全局编号。我建议你先用gpioinfo命令扫一遍,确认你的LED引脚对应哪个chip和哪条line。

小技巧: 如果你不确定引脚号,可以先写一个简单的用户态GPIO控制程序(用libgpiod),让LED亮灭一次。确认硬件没问题了,再写Overlay。这样可以避免“硬件没接对,却怀疑Overlay写错了”的尴尬。

4.2 编写第一个Overlay源文件

好,硬件确认完毕。现在我们来写Overlay。

Overlay本质上是一个设备树片段,它描述了一小段硬件。我们的目标很简单:声明一个GPIO引脚,并把它关联到一个LED设备节点上。

创建一个文件,叫my-led-overlay.dts,内容如下:

// SPDX-License-Identifier: GPL-2.0-only OR MIT
/dts-v1/;
/plugin/;

/ {
    fragment@0 {
        target = &am33xx_pinmux;
        __overlay__ {
            my_led_pins: my_led_pins {
                pinctrl-single,pins = <
                    0x24 0x07  // P8.13, GPIO0_23, 输出模式,上拉使能
                >;
            };
        };
    };

    fragment@1 {
        target = <&ocp>;
        __overlay__ {
            led_test {
                compatible = "gpio-leds";
                pinctrl-names = "default";
                pinctrl-0 = <&my_led_pins>;

                led0 {
                    label = "my:green:test";
                    gpios = <&gpio0 23 GPIO_ACTIVE_HIGH>;
                    default-state = "off";
                };
            };
        };
    };
};

我来解释一下这段代码在干什么。

第一个fragment,我们修改了引脚复用控制器(pinmux),把P8.13这个引脚配置成GPIO输出模式。第二个fragment,我们在ocp总线上添加了一个LED设备节点,它使用gpio-leds这个驱动框架。

你可能会问:为什么用两个fragment?能不能合成一个?

理论上可以,但我不建议。我在项目中吃过这个亏——把引脚配置和设备节点写在一个fragment里,结果引脚复用配置没生效,LED死活不亮。后来拆成两个fragment,先配引脚,再挂设备,问题就解决了。说白了,fragment的加载顺序是确定的,分开写更清晰,也更容易调试

4.3 编译Overlay

源文件写好了,接下来编译。我们需要设备树编译器dtc,一般系统自带。如果没有,装一下:

sudo apt-get install device-tree-compiler

编译命令很简单:

dtc -@ -I dts -O dtb -o my-led-overlay.dtbo my-led-overlay.dts

解释一下参数:

  • -@:生成overlay需要的符号信息,不加这个,加载时会报错。
  • -I dts:输入格式是dts文本。
  • -O dtb:输出格式是dtb二进制。
  • -o:指定输出文件名。注意后缀是.dtbo,这是Overlay的约定。

编译成功后,你会得到一个my-led-overlay.dtbo文件。大小大概几百字节,很小。

注意: 如果你用的是主线内核,dtc版本不能太老。我遇到过用Ubuntu 16.04自带的dtc编译出来的dtbo,加载时报“invalid overlay”错误。后来升级到1.4.7版本就好了。建议用dtc --version检查一下,低于1.4.6就升级吧。

4.4 手动加载Overlay

编译好了,怎么加载?

Linux内核提供了configfs接口来管理Overlay。路径在/sys/kernel/config/device-tree/overlays。如果你没有这个目录,说明内核没开启CONFIG_OF_OVERLAYCONFIG_CONFIGFS_FS。嗯,那就得重新编译内核了——不过大多数主流发行版都开了。

加载步骤:

  1. .dtbo文件拷贝到板子上,比如/lib/firmware/目录下。
  2. 创建一个目录,名字随便起,比如my-led
mkdir /sys/kernel/config/device-tree/overlays/my-led
  1. 把dtbo文件写入到dtbo属性文件:
cat my-led-overlay.dtbo > /sys/kernel/config/device-tree/overlays/my-led/dtbo

就这两步。如果一切顺利,LED应该亮了。

等等,为什么亮了?因为我们在Overlay里把default-state设成了"off",但gpio-leds驱动在加载时,如果检测到引脚电平是低,它不会主动拉高。那LED怎么亮的?

其实,gpio-leds驱动在probe时,会根据default-state设置初始状态。但这里有个细节:default-state = "off"只是说“初始状态是关”,但引脚电平可能因为上拉电阻或者之前的配置而处于高电平。所以LED亮不亮,取决于你板子的具体电路。

我建议你把default-state改成"on"试试,这样驱动加载时就会主动拉高引脚,LED肯定亮。

4.5 验证加载状态

加载完了,怎么确认Overlay生效了?

几个方法:

  • /sys/kernel/config/device-tree/overlays/my-led/status,如果内容是applied,说明加载成功。
  • /sys/class/leds/目录下有没有my:green:test这个子目录。如果有,说明LED设备节点被正确创建了。
  • cat /sys/kernel/debug/gpio查看GPIO状态,确认引脚方向是输出,电平符合预期。

我曾经遇到过一次:status显示applied,但/sys/class/leds/下没有设备。后来发现是gpio-leds驱动没编译进内核,而是作为模块。模块没加载,设备节点创建了也没人管。所以,别忘了检查驱动是否就绪

4.6 手动卸载Overlay

卸载比加载更简单:

rmdir /sys/kernel/config/device-tree/overlays/my-led

就这么一行。内核会撤销Overlay所做的所有修改:移除设备节点、恢复引脚复用配置、释放GPIO。LED应该会熄灭。

注意:卸载时,如果LED设备正在被用户态程序使用(比如通过sysfs操作亮度),卸载可能会失败。内核会返回-EBUSY。这时候你需要先关闭那个程序,再卸载。

经验之谈: 我调试Overlay时,经常需要反复加载卸载。我习惯写一个shell脚本,把加载、验证、卸载三步串起来。这样改一次dts,编译,跑脚本,几秒钟就能看到效果。比手动敲命令快多了。

4.7 核心逻辑流程图

下面这张图,把整个流程串起来了。从编写dts开始,到LED亮灭,每一步都对应一个内核子系统。

Overlay加载与卸载核心流程 编写 .dts 源文件 描述GPIO引脚与LED设备 dtc 编译 生成 .dtbo 二进制文件 configfs 加载 写入 dtbo 到 overlay 目录 内核 Overlay 处理过程 解析fragment → 应用pinmux配置 创建设备节点 → 匹配驱动 → probe GPIO申请 → 设置初始电平 用户态验证 检查 /sys/class/leds/ 目录 查看 GPIO 状态 / 观察 LED 卸载:rmdir overlay目录 内核撤销所有修改,LED熄灭 编写/编译 加载 内核处理 验证 卸载

4.8 常见问题与避坑

最后,我把自己踩过的坑列出来,你遇到了可以直接对照:

现象 可能原因 解决办法
加载时报-EINVAL dtbo文件格式不对,或者dtc版本太老 升级dtc,重新编译
status显示applied但LED不亮 GPIO编号错误,或者引脚复用配置不对 gpioinfo确认编号,检查pinmux寄存器
卸载时报-EBUSY LED设备被用户态程序占用 关闭占用程序,或者用fuser查找进程
加载后/sys/class/leds/没有设备 gpio-leds驱动未加载 modprobe gpio-leds,或者编译进内核

我曾经在调试一块新板子时,LED死活不亮。折腾了两小时,最后发现是GPIO的电源域没打开。嗯,有些SoC的GPIO模块需要先使能时钟和电源,否则写寄存器根本没反应。这个在设备树里通常由clockspower-domains属性控制。如果你的板子比较特殊,记得加上。

好了,第一个Overlay你已经写完了。从编写、编译、加载到卸载,整个流程走了一遍。你会发现,Overlay其实没那么神秘——它就是一段描述硬件的文本,通过内核的configfs接口动态生效。下一节,我们会在这个基础上,加入更复杂的逻辑,比如通过sysfs控制LED的亮灭和闪烁频率。


公众号:蓝海资料掘金营,微信deep3321