第4章 GPIO子系统与按键驱动
各位同学,今天我们来聊聊GPIO子系统。说实话,这是嵌入式驱动开发里最基础也最常用的部分。我刚开始做POS机外设时,第一个独立完成的驱动就是GPIO按键——虽然简单,但麻雀虽小五脏俱全。
4.1 GPIO子系统框架(gpiolib)
Linux内核的GPIO子系统,我们通常叫它gpiolib。它的核心作用就一个:统一管理GPIO资源。你想想看,不同芯片厂商的GPIO控制器千差万别,但上层驱动开发者不想关心这些细节。gpiolib就是中间人,把底层差异全封装了。
gpiolib的架构分三层:
- 底层:芯片厂商实现的gpio_chip,负责硬件操作
- 中间层:gpiolib核心代码,提供统一接口
- 上层:我们写的驱动,调用gpio_request等API
我在项目中遇到过一个问题:某款POS主板的GPIO编号和芯片手册对不上。后来发现是厂商在设备树里重新映射了编号。嗯,这里要注意——永远不要硬编码GPIO号,要用设备树或平台数据来传递。
4.2 GPIO控制接口
gpiolib提供了一套标准的API,我挑几个最常用的说说。
4.2.1 申请与释放
int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);
gpio_request是第一步。我个人习惯在probe函数里申请,在remove函数里释放。label参数建议写清楚用途,比如"power_key"、"paper_out_sensor",方便调试时查看。
4.2.2 方向设置
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);
按键驱动一般用输入模式。但有个细节:设置方向前,GPIO的默认状态是什么?有些芯片默认是输出低电平,如果你直接设成输入,外部电路可能已经出问题了。
4.2.3 读写值
int gpio_get_value(unsigned gpio);
void gpio_set_value(unsigned gpio, int value);
这两个函数很简单,但要注意:gpio_get_value返回的是逻辑值,不是物理电平。如果外部有上拉或下拉,返回值可能和你预想的不一样。
4.3 中断注册
按键驱动最核心的就是中断。轮询?那太浪费CPU了。POS机对响应速度有要求,用户按一下键,你得立刻反应。
4.3.1 申请中断号
int gpio_to_irq(unsigned gpio);
这个函数把GPIO号转成中断号。我建议你不要假设中断号等于GPIO号,虽然有些芯片确实这么设计,但大部分不是。
4.3.2 注册中断处理函数
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long flags, const char *name, void *dev);
参数里flags很关键。按键一般用IRQF_TRIGGER_FALLING或IRQF_TRIGGER_RISING。但有个坑:如果按键按下时电平抖动,你会收到多次中断。这就是所谓的"按键抖动"问题。
4.4 实现一个GPIO按键驱动
好了,理论说完了,咱们动手写一个。这个驱动很简单:检测按键按下,打印一条日志,并通过input子系统上报按键事件。
4.4.1 设备树节点
key_device {
compatible = "my-company,gpio-key";
gpios = &gpio1 15 GPIO_ACTIVE_LOW;
interrupt-parent = <&gpio1>;
interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
label = "power_key";
};
4.4.2 驱动代码框架
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/input.h>
struct gpio_key_data {
int gpio;
int irq;
struct input_dev *input;
};
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
struct gpio_key_data *data = dev_id;
int val = gpio_get_value(data->gpio);
// 上报按键事件
input_report_key(data->input, KEY_POWER, !val);
input_sync(data->input);
return IRQ_HANDLED;
}
static int gpio_key_probe(struct platform_device *pdev)
{
struct gpio_key_data *data;
int ret;
data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 获取GPIO
data->gpio = of_get_named_gpio(pdev->dev.of_node, "gpios", 0);
ret = devm_gpio_request(&pdev->dev, data->gpio, "gpio_key");
if (ret)
return ret;
gpio_direction_input(data->gpio);
// 获取中断号并注册
data->irq = gpio_to_irq(data->gpio);
ret = devm_request_irq(&pdev->dev, data->irq, gpio_key_isr,
IRQF_TRIGGER_FALLING, "gpio_key", data);
if (ret)
return ret;
// 注册input设备
data->input = devm_input_allocate_device(&pdev->dev);
data->input->name = "GPIO Key";
set_bit(EV_KEY, data->input->evbit);
set_bit(KEY_POWER, data->input->keybit);
ret = input_register_device(data->input);
if (ret)
return ret;
platform_set_drvdata(pdev, data);
return 0;
}
static const struct of_device_id gpio_key_of_match[] = {
{ .compatible = "my-company,gpio-key" },
{ }
};
MODULE_DEVICE_TABLE(of, gpio_key_of_match);
static struct platform_driver gpio_key_driver = {
.probe = gpio_key_probe,
.driver = {
.name = "gpio_key",
.of_match_table = gpio_key_of_match,
},
};
module_platform_driver(gpio_key_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple GPIO key driver");
4.4.3 关键点说明
- devm_系列函数:自动资源管理,probe失败或remove时自动释放。我强烈建议用这个,省得你忘了free。
- input子系统:按键事件通过input子系统上报,上层应用(比如Android的KeyEvent)就能收到。
- 中断标志:这里用了IRQF_TRIGGER_FALLING,因为设备树里GPIO_ACTIVE_LOW,按键按下时电平从高变低。
避坑指南:我曾经在某个项目里,按键按下后系统卡死。查了半天,发现是中断处理函数里调用了可能导致睡眠的函数(比如printk在特定配置下会睡眠)。记住:中断上下文不能睡眠。如果要做复杂操作,用工作队列或tasklet。
4.5 调试与验证
驱动写完了,怎么验证?我一般这么做:
- 加载驱动后,查看
/proc/interrupts,确认中断号已注册 - 按一下按键,用
cat /proc/interrupts看中断计数是否增加 - 用
evtest工具监听input事件,看是否能收到按键上报
如果中断计数没变,先检查硬件连接——用万用表量一下GPIO引脚电平。如果硬件没问题,再查设备树里的GPIO号对不对。我遇到过最离谱的一次,是设备树里GPIO号写对了,但引脚复用配置错了,GPIO功能没使能。
4.6 小结
这一章我们走通了GPIO按键驱动的完整流程。从gpiolib框架到中断注册,再到实际代码实现。说实话,这个驱动虽然简单,但它是理解Linux驱动模型的好例子。你掌握了它,后面学I2C、SPI驱动就会轻松很多。
下一章我们会讲定时器和PWM输出,到时候会用到本章的知识。嗯,先消化一下,有问题随时交流。