第四节:编写第一个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。
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_OVERLAY和CONFIG_CONFIGFS_FS。嗯,那就得重新编译内核了——不过大多数主流发行版都开了。
加载步骤:
- 把
.dtbo文件拷贝到板子上,比如/lib/firmware/目录下。 - 创建一个目录,名字随便起,比如
my-led:
mkdir /sys/kernel/config/device-tree/overlays/my-led
- 把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。这时候你需要先关闭那个程序,再卸载。
4.7 核心逻辑流程图
下面这张图,把整个流程串起来了。从编写dts开始,到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模块需要先使能时钟和电源,否则写寄存器根本没反应。这个在设备树里通常由clocks和power-domains属性控制。如果你的板子比较特殊,记得加上。
好了,第一个Overlay你已经写完了。从编写、编译、加载到卸载,整个流程走了一遍。你会发现,Overlay其实没那么神秘——它就是一段描述硬件的文本,通过内核的configfs接口动态生效。下一节,我们会在这个基础上,加入更复杂的逻辑,比如通过sysfs控制LED的亮灭和闪烁频率。
公众号:蓝海资料掘金营,微信deep3321