"这玩意儿在设备树里配置一下就行了。"——说这话的人,一定没踩过完整一遍坑。
一个月前接了个活,某个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先自己走一遍完整流程。