第3章:GPIO驱动开发:GPIO控制器原理、Linux GPIO子系统框架、编写简单的GPIO驱动
好,咱们今天聊聊GPIO驱动。说实话,GPIO是SoC里最简单也最常用的外设之一。但你别小看它,很多复杂外设的驱动,底层都离不开GPIO的控制。我刚开始做嵌入式时,觉得GPIO不就是拉高拉低嘛,有啥好学的?后来踩了不少坑,才发现这里面的门道还真不少。
3.1 GPIO控制器原理
GPIO,全称是General Purpose Input/Output。说白了,就是芯片上那些可以编程控制的引脚。你可以把它配置成输入,读取外部电平;也可以配置成输出,控制外部设备。
一个典型的GPIO控制器,内部结构大概长这样:
- 数据寄存器:用来读写引脚电平。写1输出高,写0输出低。读的话,就是获取当前引脚电平。
- 方向寄存器:决定引脚是输入还是输出。一般写1是输出,写0是输入。嗯,不同厂商可能相反,要注意。
- 上拉/下拉寄存器:配置内部的上拉或下拉电阻。防止引脚浮空,产生不确定电平。
- 中断寄存器:配置引脚的中断触发方式,比如上升沿、下降沿、高电平、低电平。
我在项目中遇到过一个问题:某款芯片的GPIO默认是输入模式,而且内部下拉电阻是使能的。结果我直接配置成输出,发现电平拉不高。查了半天,原来是下拉电阻在作怪。所以,操作GPIO前,一定要先搞清楚复位状态。
核心要点:GPIO控制器的本质,就是一组内存映射的寄存器。你往特定地址写值,就能控制引脚。读特定地址,就能获取引脚状态。
3.2 Linux GPIO子系统框架
Linux内核为了统一管理GPIO,搞了一套GPIO子系统。这套框架分三层:
- GPIO控制器驱动层:这是芯片厂商要写的。负责操作具体的硬件寄存器。
- GPIO核心层:内核提供的通用接口。管理GPIO的申请、释放、方向设置、读写等。
- GPIO使用者层:其他驱动或用户空间程序。通过标准API来使用GPIO。
你想想看,如果没有这个框架,每个驱动都直接操作寄存器,那代码得多乱?而且不同芯片的寄存器地址还不一样,移植起来简直要命。
我个人习惯,在写GPIO驱动前,先看看芯片手册里的GPIO控制器章节。重点关注寄存器偏移地址和位定义。然后对照内核里的pinctrl子系统和GPIO子系统,看看有没有现成的驱动可以参考。
小提示:现在的SoC,GPIO通常和pinctrl(引脚控制)绑定在一起。配置GPIO时,往往要先通过pinctrl把引脚复用成GPIO功能。这个顺序别搞反了。
3.3 编写简单的GPIO驱动
好,理论说完了,咱们来点实际的。写一个最简单的GPIO驱动,控制一个LED灯闪烁。
首先,你得在设备树里描述GPIO资源。比如:
led-gpios = &gpio1 15 GPIO_ACTIVE_HIGH;
这行代码的意思是:使用gpio1控制器的第15号引脚,高电平有效。
然后,在驱动代码里,通过GPIO子系统API来操作:
#include <linux/gpio.h>
#include <linux/of_gpio.h>
static int led_gpio;
static struct timer_list led_timer;
static void led_timer_callback(struct timer_list *t)
{
int val;
val = gpio_get_value(led_gpio);
gpio_set_value(led_gpio, !val);
mod_timer(&led_timer, jiffies + msecs_to_jiffies(500));
}
static int led_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
led_gpio = of_get_named_gpio(dev->of_node, "led-gpios", 0);
if (!gpio_is_valid(led_gpio)) {
dev_err(dev, "invalid GPIO\n");
return -EINVAL;
}
gpio_request(led_gpio, "led");
gpio_direction_output(led_gpio, 0);
timer_setup(&led_timer, led_timer_callback, 0);
mod_timer(&led_timer, jiffies + msecs_to_jiffies(500));
return 0;
}
static int led_remove(struct platform_device *pdev)
{
gpio_set_value(led_gpio, 0);
gpio_free(led_gpio);
del_timer(&led_timer);
return 0;
}
static const struct of_device_id led_of_match[] = {
{ .compatible = "my-led-driver" },
{ }
};
MODULE_DEVICE_TABLE(of, led_of_match);
static struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "my_led",
.of_match_table = led_of_match,
},
};
module_platform_driver(led_driver);
这段代码做了几件事:
- 从设备树里获取GPIO编号。
- 申请GPIO资源,并设置为输出,初始低电平。
- 启动一个定时器,每500ms翻转一次GPIO电平。
- 卸载驱动时,释放GPIO并停止定时器。
注意:gpio_request()一定要检查返回值。我曾经在一个项目里忘了检查,结果GPIO被其他驱动占用了,导致两个驱动互相干扰,系统行为变得很奇怪。这种bug很难查,因为现象不固定。
嗯,这里还要提一点。现在的内核更推荐使用GPIO描述符(Descriptor)接口,而不是传统的整数接口。比如用gpiod_get()代替gpio_request()。好处是更安全,能自动处理设备树里的极性标志。
举个例子:
struct gpio_desc *led_desc;
led_desc = gpiod_get(dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(led_desc)) {
return PTR_ERR(led_desc);
}
gpiod_set_value(led_desc, 1); // 输出高电平
gpiod_set_value(led_desc, 0); // 输出低电平
你看,代码更简洁了。而且gpiod_set_value()会自动处理GPIO_ACTIVE_LOW的情况。如果你在设备树里标记了低电平有效,那写1时,引脚实际输出的是低电平。这个细节,很多新手会忽略。
最后,总结一下GPIO驱动的开发流程:
- 第一步:看芯片手册,了解GPIO控制器的寄存器布局。
- 第二步:配置设备树,描述GPIO资源。
- 第三步:在probe函数里,通过GPIO子系统API申请和配置GPIO。
- 第四步:实现具体的功能逻辑。
- 第五步:在remove函数里,释放GPIO资源。
我曾经见过一个驱动,probe里申请了GPIO,但remove里忘了释放。结果模块卸载后,那个GPIO还处于被占用的状态,其他驱动用不了。这种问题,用gpioinfo命令一看就能发现。
好了,GPIO驱动的基础就这些。下一章,咱们聊聊更复杂的中断驱动。到时候你会发现,GPIO中断的配置,其实也是GPIO驱动的一部分。