接手一个新项目,硬件那边扔过来一块imx6ull的板子,说"就是加点GPIO控制,很简单"。我当时也天真地信了。
结果呢?一个GPIO输出高电平,折腾了我整整一个工作日。
如果你也搞嵌入式的Linux开发,设备树这玩意儿一定绕不开。说实话,我刚入行那会儿觉得设备树就是个"配置文件",后来发现这东西比我想象的坑多得多。
先说我遇到的情况。板上接了一个LED,原理图清清楚楚标着——引脚是GPIO1_IO03。按照经验,我翻出芯片手册,查到GPIO1基地址是0x209C000,然后写设备树节点:
my_led {
compatible = "my-led";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
led-gpio = <&gpio1 3 GPIO_ACTIVE_HIGH>;
status = "okay";
};
pinmux配置也在iomuxc节点里补上了:
pinctrl_led: ledgrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10b0
>;
};
编译设备树,烧进去,启动,一切看起来正常。sysfs里也能看到节点,gpio号也对的。
然后写了个测试程序:
int fd = open("/sys/class/gpio/gpio3/value", O_WRONLY);
if (fd < 0) {
perror("open gpio");
return -1;
}
write(fd, "1", 1);
close(fd);
运行。LED没亮。心里咯噔一下。
拿万用表一量,引脚电平——0V。不是硬件问题,线没接错,电平就是拉不起来。
折腾了半天,试了好几种方案:
先是怀疑驱动没加载对,dmesg翻了好几遍,没有报错。又怀疑是pinmux被别的驱动抢占了,在设备树里加了disable属性别的节点,没用。
后来甚至怀疑自己焊锡短路了,把板子翻来覆去看了半天。
最后怎么找到问题的?
我在一个Linux邮件列表的老帖子里看到一句话——"Make sure you don't miss the angle brackets in the GPIO specifier."
回头一看自己的设备树:
led-gpio = <&gpio1 3 GPIO_ACTIVE_HIGH>;
等等。GPIO1_IO03,这个"3"是哪个3?
imx6ull的GPIO控制器里,每个bank有32个引脚,但GPIO1的序号并不是0-31直接对应硬件引脚号。有些SoC的GPIO控制器内部偏移和外部引脚号不一样,而imx6ull的gpio1节点里有一个#gpio-cells = <2>,第二个cell是硬件引脚号。
我重新查了原理图和芯片手册,GPIO1_IO03这个3,确实是GPIO1里的第3个引脚,按说没错啊?
再仔细看。我用的内核版本是4.19,这个版本的imx-gpio驱动里,设备树解析gpio时,第二个参数不是直接当引脚号用,它要经过一个内部的偏移换算。GPIO1在imx6ull上实际对应的硬件基址偏移是需要计算的。
换成这样就好了:
led-gpio = <&gpio1 0 GPIO_ACTIVE_HIGH>;
哈?从3改成0就好了?
对。因为GPIO1_IO03对驱动来说,它要找的是"GPIO1 bank内部的第3个pin",而<&gpio1 3>这种写法,驱动里的of_get_named_gpio_flags会把第二个参数当成一个"硬件port序号",你得按gpio_chip的base来算。那会儿gpio1的base是0,所以引脚号3就是GPIO1_IO03的Linux gpio号——但这不代表设备树里写3就能用。
后来我在驱动里加了一行打印,发现gpio_request拿到的号是3没错,但pinctrl子系统根本没把那个pinmux配上去。问题出在pinctrl的引用上。
真正的原因其实很简单——设备树里pinctrl的引用写错了名字,驱动加载pinmux的时候没拉到对应的配置。
改:
pinctrl-0 = <&pinctrl_led>;
pinctrl-names = "default";
这两行看起来没问题,但问题是前面的pinmux节点名字:
pinctrl_led: led-grp {
注意到了吗?我前面写的是ledgrp,但这里写的是led-grp。虽然设备树语法上这俩都是合法label,但pinctrl子系统按名字匹配时,采的是pinctrl-0的phandle而不是名字匹配,所以这本不应该有问题。
等等,那真正的原因到底是什么?
我把整个节点都删了重新写,然后一行行对照。最后发现:
my_led {
compatible = "my-led";
…
};
内核里根本没有"my-led"这个驱动的compatible啊!我要控制GPIO,正确的做法是用gpio-leds这个内核自带的驱动,或者在用户空间通过/sys/class/gpio操作,而不是自己去设备树里编一个节点等驱动来binding。
对,这就是我最蠢的地方——我定义了一个设备树节点,但没有写对应的platform_driver,内核压根不认识"my-led",当然不会帮我配置pinmux。
回头加了驱动:
static const struct of_device_id my_led_of_match[] = {
{ .compatible = "my-led" },
{ }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);
static struct platform_driver my_led_driver = {
.driver = {
.name = "my-led",
.of_match_table = my_led_of_match,
},
.probe = my_led_probe,
.remove = my_led_remove,
};
重新编译、烧录、启动——LED亮了,亮得刺眼。
回过头来看,一个GPIO折腾一天,说出去都丢人。但做嵌入式Linux开发的,谁还没被设备树坑过几次呢?
设备树这个东西,说简单也简单,说复杂也复杂。它的语法本身不复杂,但内核解析设备树的方式、各个子系统对设备树属性的解释规则,每个SoC平台还不一样。imx6ull和stm32mp1的设备树写法就有差异,同一种写法在这个平台能用,换个平台可能就翻车。
后来我养成了一个习惯:每次改设备树的时候,都拿dtc -I dtb -O dts反编译一下看看实际解析出来的是什么样,确认没少符号、没写错名字。
至于那些奇怪的属性名和值怎么查?多看Documentation/devicetree/bindings/下的文档,还有内核源码里的of_开头的函数。有时候查邮件列表比查文档还管用。
毕竟做嵌入式嘛,踩坑才是常态,同一个坑不踩第二次就算进步了。



