当前位置:首页>Linux>嵌入式Linux GPIO驱动翻车记——设备树里改了一下午

嵌入式Linux GPIO驱动翻车记——设备树里改了一下午

  • 2026-10-11 07:04:01
嵌入式Linux GPIO驱动翻车记——设备树里改了一下午

"这玩意儿在设备树里配置一下就行了。"——说这话的人,一定没踩过完整一遍坑。

一个月前接了个活,某个ARM板子上要控制一路GPIO输出,高低电平切换,配个LED指示灯。心想多大事儿,不就是设备树里加个节点,写个简单的字符驱动,echo 1 > /sys/class/leds/xxx/brightness 完事。

结果从下午两点折腾到晚上十点。


先看设备树。我用的这个SoC,GPIO控制器注册在pinctrl下面,每个bank有32个pin。按理说加个gpio-leds节点就行:

leds {
    compatible = "gpio-leds";
    status_led {
        label = "status:green";
        gpios = <&gpio3 15 GPIO_ACTIVE_HIGH>;
        linux,default-trigger = "heartbeat";
    };
};

编译设备树,烧录,重启。ls /sys/class/leds/ 一看,空的。

这就开始折腾了。

第一反应:设备树语法错了?ftdump 看了一下编译后的dtb,节点确实在。但/sys下面没创出来,大概率是驱动没匹配上。查内核配置,CONFIG_LEDS_GPIO没有开。

好家伙,这板子默认核心里没编这个驱动。

于是重新编内核。make menuconfig,Device Drivers → LED Support → LED Support for GPIO connected LEDs,编成模块。

make -j8 zImage modules
make modules_install
depmod -a
reboot

然后 modprobe leds_gpio。再 ls /sys/class/leds/,出来了。

但另一个问题来了:我想在用户态直接操作这个GPIO,不用LED子系统,自己写个驱动来控制时序。有次做PWM模拟的时候,发现LED子系统的brightness有个内部反转逻辑,跟你想的不一样。

所以又写了这么个玩意儿:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_gpio.h>

#define DEVICE_NAME "mygpio"
#define CLASS_NAME  "mygpio"

static int major;
static struct class *mygpio_class = NULL;
static int gpio_pin = -1;

static ssize_t mygpio_write(struct file *filp, const char __user *buf,
                            size_t count, loff_t *ppos)

{
    char kbuf[4];
    unsigned long val;

    if (count > 3) count = 3;
    if (copy_from_user(kbuf, buf, count))
        return -EFAULT;

    kbuf[count] = '\0';
    if (kstrtoul(kbuf, 10, &val))
        return -EINVAL;

    gpio_set_value(gpio_pin, val ? 1 : 0);
    return count;
}

写的时候注意了,copy_from_user之前得检查buffer长度。之前有内核模块因为没检查直接panic,整个板子重启,别提多尴尬了。

然后就是设备树里要加自己的compatible:

mygpio {
    compatible = "mygpio,ctrl";
    mygpio-gpios = <&gpio3 15 GPIO_ACTIVE_HIGH>;
};

驱动里用 of_get_named_gpio 来解析:

static int mygpio_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;

    gpio_pin = of_get_named_gpio(np, "mygpio-gpios", 0);
    if (gpio_pin < 0) {
        dev_err(dev, "failed to get gpio: %d\n", gpio_pin);
        return gpio_pin;
    }

    if (gpio_request(gpio_pin, "mygpio")) {
        dev_err(dev, "gpio request failed\n");
        return -EBUSY;
    }
    gpio_direction_output(gpio_pin, 0);
    return 0;
}

折腾完发现,最坑的其实不是代码本身,是Makefile。内核模块的Makefile有一堆讲究,KERNEL_DIR、交叉编译链、arch、CROSS_COMPILE,但凡有一个少设了,编出来的模块要么insmod报错invalid format,要么符号找不到。

obj-m := mygpio.o
KERNEL_DIR := /home/ubuntu/linux-4.19
CROSS_COMPILE := arm-linux-gnueabihf-
ARCH := arm

all:
    $(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) \
        CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=$(ARCH) clean

有次CROSS_COMPILE忘了设,编出来x86架构的.ko,insmod的时候提示"wrong ELF class",查了半小时才反应过来——板子是ARM的,我特么编了个x86的模块上去。

最后echo 1 > /dev/mygpio,灯亮了。就一个灯,折腾了一个大半天。

嵌入式Linux就这样——上层看着简单,每层都有坑。设备树、内核配置、驱动框架、Makefile、交叉编译,一条链上任何一个环节断了你都跑不通。下次谁再说"配个GPIO很简单",我建议ta先自己走一遍完整流程。

最新文章

随机文章