3、GPIO驱动实战:从框架到LED点亮
GPIO驱动,说白了就是嵌入式Linux里最基础也最常用的外设操作。你想想看,一个嵌入式板子上,控制LED、读取按键、驱动蜂鸣器,哪个离得开GPIO?我刚开始做驱动时,总觉得GPIO太简单,结果在项目里踩了不少坑。嗯,这一章咱们就把GPIO子系统彻底讲透。
3.1 GPIO子系统框架:三层结构
Linux内核的GPIO子系统,其实就三层。我习惯把它想象成一个「三层蛋糕」:
- 最底层:硬件相关层,直接操作GPIO寄存器
- 中间层:GPIO核心层,提供统一的接口管理
- 最上层:驱动使用层,也就是我们写驱动时调用的API
为什么会这样设计?说白了就是为了解耦。你换了一个芯片,只需要改底层,上层代码完全不用动。我在项目中遇到过,从i.MX6换到i.MX8,LED驱动代码一行没改,只改了设备树。
核心要点:GPIO子系统通过gpiolib实现,提供gpio_request()、gpio_direction_output()、gpio_set_value()等标准接口。驱动开发者只需要调用这些API,不需要关心底层寄存器。
下面这张图,是我画的三层架构关系,你看一眼就明白了:
3.2 设备树中的GPIO描述
设备树里描述GPIO,其实就几个关键属性。我刚开始学的时候,总搞混gpios和gpio的区别。后来踩了一次坑才记住——gpios是复数,表示可以有多个GPIO;gpio是单数,只表示一个。
来看一个典型的LED节点描述:
/ {
leds {
compatible = "gpio-leds";
led0: led-red {
label = "red-led";
gpios = &gpio1 3 GPIO_ACTIVE_LOW;
default-state = "off";
};
led1: led-green {
label = "green-led";
gpios = &gpio1 4 GPIO_ACTIVE_HIGH;
default-state = "on";
};
};
};
这里有几个关键点,我一个个说:
- compatible:匹配驱动,内核里已经有
gpio-leds驱动,可以直接用 - gpios:三个参数——GPIO控制器、引脚号、有效电平标志
- GPIO_ACTIVE_LOW:低电平有效,意味着写0时灯亮,写1时灯灭
- GPIO_ACTIVE_HIGH:高电平有效,写1亮,写0灭
我的习惯:设备树里尽量用GPIO_ACTIVE_LOW或GPIO_ACTIVE_HIGH宏定义,不要直接写0或1。这样代码可读性高,别人一看就知道电平逻辑。
3.3 GPIO操作核心API
写GPIO驱动,最常用的就这几个API。我列个表格,方便你对照:
| API函数 | 功能说明 | 返回值 |
|---|---|---|
gpio_request(unsigned gpio, const char *label) |
申请GPIO资源,防止被其他模块占用 | 0成功,负值失败 |
gpio_direction_output(unsigned gpio, int value) |
设置GPIO为输出方向,并设置初始电平 | 0成功,负值失败 |
gpio_direction_input(unsigned gpio) |
设置GPIO为输入方向 | 0成功,负值失败 |
gpio_set_value(unsigned gpio, int value) |
设置GPIO输出电平(0或1) | 无返回值 |
gpio_get_value(unsigned gpio) |
读取GPIO当前电平 | 0或1 |
gpio_free(unsigned gpio) |
释放GPIO资源 | 无返回值 |
注意:gpio_request()一定要在操作GPIO之前调用。我曾经在项目里忘了调用,结果两个驱动同时操作同一个引脚,导致LED忽明忽暗,查了两天才找到原因。
3.4 实现LED驱动:完整代码
好了,理论说完了,咱们直接上代码。这是一个完整的LED驱动,我加了详细的注释:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#define LED_GPIO 3 // 假设使用GPIO1_3
static int __init led_driver_init(void)
{
int ret;
// 第一步:申请GPIO资源
ret = gpio_request(LED_GPIO, "my-led");
if (ret) {
pr_err("Failed to request GPIO %d\n", LED_GPIO);
return ret;
}
// 第二步:设置输出方向,初始为低电平
ret = gpio_direction_output(LED_GPIO, 0);
if (ret) {
pr_err("Failed to set GPIO direction\n");
gpio_free(LED_GPIO);
return ret;
}
pr_info("LED driver initialized successfully\n");
// 测试:闪烁3次
int i;
for (i = 0; i < 3; i++) {
gpio_set_value(LED_GPIO, 1); // 亮
mdelay(500);
gpio_set_value(LED_GPIO, 0); // 灭
mdelay(500);
}
return 0;
}
static void __exit led_driver_exit(void)
{
gpio_set_value(LED_GPIO, 0); // 关闭LED
gpio_free(LED_GPIO); // 释放GPIO
pr_info("LED driver exited\n");
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple LED GPIO Driver");
3.5 驱动加载与测试
代码写完了,怎么验证?我一般按这个步骤来:
- 编译驱动:把代码放到内核源码的drivers目录下,或者作为外部模块编译
- 加载模块:
insmod led_driver.ko - 查看日志:
dmesg | tail,看有没有错误信息 - 检查GPIO状态:
cat /sys/kernel/debug/gpio,查看引脚是否被正确申请 - 卸载模块:
rmmod led_driver,确认LED熄灭
调试技巧:如果LED不亮,先检查设备树里GPIO号是否正确。我常用的方法是,在/sys/class/gpio目录下手动操作一下,确认硬件没问题,再查驱动代码。
3.6 常见故障与定位
做GPIO驱动,难免遇到问题。我总结了几种常见情况:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| gpio_request返回-EBUSY | GPIO已被其他驱动占用 | 检查/sys/kernel/debug/gpio,看谁在用 |
| LED不亮 | GPIO号错误或电平逻辑反了 | 用gpio_set_value手动切换,万用表测电压 |
| LED亮度异常 | 引脚复用配置错误 | 检查设备树pinctrl设置 |
| 卸载后LED仍亮 | 没有在exit函数里关闭 | 检查gpio_set_value和gpio_free调用顺序 |
我曾经遇到过一个特别坑的问题:LED在加载驱动时正常闪烁,但卸载后再加载就不亮了。查了半天,发现是gpio_free之后,引脚被复用成了其他功能。解决办法是在设备树里加上pinctrl-0属性,强制指定引脚功能。
我的建议:写GPIO驱动时,一定要在init函数里检查每个API的返回值。很多新手觉得gpio_request不会失败,结果在量产时出了问题。宁可多写几行错误处理,也别偷懒。