4、GPIO驱动开发:从框架到实战
GPIO,说白了就是芯片的“手脚”。你想让灯亮,它就输出高电平;你想检测按键,它就读取电平变化。在机顶盒里,GPIO控制着红外接收头、前面板LED、复位按键……几乎无处不在。
这一章,我们就把GPIO驱动开发的完整链路走一遍。从内核框架,到API调用,再到设备树描述,最后写一个带中断的按键驱动。嗯,都是实战干货。
4.1 GPIO子系统框架:别被“分层”吓到
Linux GPIO子系统分三层:
- 硬件层:芯片厂商实现的gpio_chip,负责读写寄存器
- 核心层:gpiolib,提供统一的API接口
- 用户层:/sys/class/gpio 或者字符设备
我个人习惯把核心层看作“中间人”。你写驱动时,基本只跟gpiolib打交道。它帮你屏蔽了不同芯片的差异。
关键数据结构:
struct gpio_chip {
const char *label;
int base; // GPIO编号起始值
int ngpio; // 本芯片管理的GPIO数量
int (*direction_input)(struct gpio_chip *gc, unsigned offset);
int (*direction_output)(struct gpio_chip *gc, unsigned offset, int value);
int (*get)(struct gpio_chip *gc, unsigned offset);
void (*set)(struct gpio_chip *gc, unsigned offset, int value);
int (*to_irq)(struct gpio_chip *gc, unsigned offset);
// ... 还有不少,但上面这几个最常用
};
我在项目中遇到过,有些芯片的to_irq接口没实现,导致GPIO无法申请中断。排查了半天,最后发现是芯片BSP包没更新。所以拿到新板子,先检查这个回调函数有没有注册。
4.2 gpiolib API使用:记住这几个就够了
gpiolib提供的API,说白了就三类:申请、读写、释放。我整理了一张速查表:
| 功能 | API | 说明 |
|---|---|---|
| 申请GPIO | gpio_request(unsigned gpio, const char *label) | 返回0表示成功 |
| 设置方向 | gpio_direction_input(gpio) | 输入模式 |
| 设置方向 | gpio_direction_output(gpio, value) | 输出模式,同时设置初始值 |
| 读取电平 | gpio_get_value(gpio) | 返回0或1 |
| 设置电平 | gpio_set_value(gpio, value) | 0或1 |
| 申请中断 | gpio_to_irq(gpio) | 返回中断号 |
| 释放GPIO | gpio_free(gpio) | 别忘了调用 |
小技巧:gpio_request失败时,用cat /sys/kernel/debug/gpio查看哪些GPIO已被占用。我调试时经常用这招。
你想想看,如果只是点个灯,代码其实就几行:
gpio_request(100, "my_led");
gpio_direction_output(100, 0);
gpio_set_value(100, 1); // 亮灯
// 用完后
gpio_set_value(100, 0);
gpio_free(100);
但实际项目中,我建议用devm_gpio_request这类资源管理版本。它会在驱动卸载时自动释放,省得你忘了调用gpio_free。
4.3 在设备树中描述GPIO:让硬件“说话”
设备树就是硬件的“身份证”。GPIO在设备树里怎么描述?看个例子:
/ {
leds {
compatible = "gpio-leds";
power_led {
label = "power";
gpios = &gpio1 5 GPIO_ACTIVE_HIGH;
default-state = "on";
};
};
keys {
compatible = "gpio-keys";
power_key {
label = "power key";
gpios = &gpio2 3 GPIO_ACTIVE_LOW;
linux,code = <KEY_POWER>;
};
};
};
这里要注意几点:
gpios = &gpio1 5 GPIO_ACTIVE_HIGH表示使用gpio1控制器,偏移5,高电平有效GPIO_ACTIVE_HIGH和GPIO_ACTIVE_LOW是宏定义,告诉内核电平极性- gpio-keys是内核自带的驱动,你只需要在设备树里描述按键属性
我曾经踩过一个坑:设备树里写的是GPIO_ACTIVE_LOW,但电路设计是按键按下拉低。驱动里又做了一次取反,结果逻辑反了。嗯,这里要记住:设备树描述的是硬件行为,驱动里不要再额外处理。
4.4 编写GPIO按键驱动:从零开始
好,我们动手写一个完整的按键驱动。它检测GPIO电平变化,上报给输入子系统。
#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/input.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
struct my_key_data {
int gpio;
int irq;
int key_code;
struct input_dev *input;
};
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
struct my_key_data *data = dev_id;
int val;
val = gpio_get_value(data->gpio);
// 上报按键事件
input_report_key(data->input, data->key_code, !val);
input_sync(data->input);
return IRQ_HANDLED;
}
static int my_key_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct my_key_data *data;
int ret;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 从设备树获取GPIO
data->gpio = of_get_named_gpio(dev->of_node, "key-gpios", 0);
if (!gpio_is_valid(data->gpio)) {
dev_err(dev, "invalid GPIO\n");
return -EINVAL;
}
// 申请GPIO并设置为输入
ret = devm_gpio_request_one(dev, data->gpio, GPIOF_IN, "my_key");
if (ret) {
dev_err(dev, "gpio request failed\n");
return ret;
}
// 获取中断号
data->irq = gpio_to_irq(data->gpio);
if (data->irq < 0) {
dev_err(dev, "gpio_to_irq failed\n");
return data->irq;
}
// 申请中断
ret = devm_request_irq(dev, data->irq, key_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"my_key", data);
if (ret) {
dev_err(dev, "irq request failed\n");
return ret;
}
// 注册输入设备
data->input = devm_input_allocate_device(dev);
if (!data->input)
return -ENOMEM;
data->input->name = "my_key";
set_bit(EV_KEY, data->input->evbit);
set_bit(KEY_POWER, data->input->keybit);
ret = input_register_device(data->input);
if (ret) {
dev_err(dev, "input register failed\n");
return ret;
}
platform_set_drvdata(pdev, data);
dev_info(dev, "my key driver loaded\n");
return 0;
}
static const struct of_device_id my_key_of_match[] = {
{ .compatible = "my-company,my-key" },
{ }
};
MODULE_DEVICE_TABLE(of, my_key_of_match);
static struct platform_driver my_key_driver = {
.probe = my_key_probe,
.driver = {
.name = "my_key",
.of_match_table = my_key_of_match,
},
};
module_platform_driver(my_key_driver);
MODULE_LICENSE("GPL");
这段代码里,我用了devm_系列函数,省去了手动释放资源的麻烦。中断处理函数里,读取GPIO电平并取反——因为设备树里定义的是低电平有效。
注意:中断处理函数里不要做耗时操作。按键消抖、长按检测这些逻辑,建议放到用户空间或者用内核的input子系统自带功能处理。
4.5 中断处理:别让CPU空转
GPIO按键驱动里,中断处理是核心。为什么不用轮询?你想想看,如果CPU每秒轮询100次,就为了等一个按键,那得多浪费。
中断处理分上半部和下半部:
- 上半部:快速处理,标记中断状态。比如读取GPIO电平,上报事件。
- 下半部:处理耗时操作。比如按键消抖、长按检测。可以用tasklet、workqueue或threaded IRQ。
我个人习惯用threaded IRQ,它把中断处理放到内核线程里,可以睡眠,适合做复杂操作:
ret = devm_request_threaded_irq(dev, irq, NULL,
key_irq_handler_thread,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"my_key", data);
第一个参数是上半部处理函数,传NULL表示不用上半部。第二个参数是下半部线程函数。这样写,代码更清晰。
我曾经在项目里遇到一个问题:按键按下时,中断触发多次。原因是按键抖动,电平在短时间内跳变多次。解决方案是在硬件上加RC滤波,或者在软件里做消抖——比如在中断触发后,延迟20ms再读取电平。
避坑指南:我曾经在中断处理函数里调用了printk,结果系统直接卡死。因为printk可能引起调度,而中断上下文不允许睡眠。调试时用pr_debug或者tracepoint,别用printk。
4.6 总结:GPIO驱动开发的“三板斧”
回顾一下,GPIO驱动开发其实就三步:
- 设备树描述:告诉内核,哪个引脚是什么功能
- 驱动初始化:申请GPIO、设置方向、注册中断
- 中断处理:读取电平、上报事件、处理抖动
嗯,掌握了这些,机顶盒上常见的GPIO按键、LED、红外接收头驱动,你都能轻松搞定。下一章,我们会深入I2C驱动开发,那又是另一片天地了。