4. GPIO子系统驱动开发:从框架到实战
GPIO,说白了就是芯片的“手脚”。你要控制LED亮灭、检测按键按下,都得靠它。在Linux里,GPIO驱动已经封装得很好了,我们不需要直接操作寄存器——除非你想自己造轮子。
我个人习惯,拿到一个新板子,先看GPIO的用法。因为这是最基础的外设,也是验证硬件是否正常的第一步。今天我们就从框架到实战,把GPIO子系统彻底搞明白。
4.1 GPIO子系统框架:三层结构
Linux的GPIO子系统分三层,我画了个图,你看一眼就懂了:
用户空间通过/sys/class/gpio或libgpiod操作GPIO。内核空间里,gpiolib负责管理所有GPIO。硬件层就是芯片原厂提供的驱动,全志T113的GPIO控制器驱动已经写好了,我们只需要配置设备树,然后调用API就行。
4.2 设备树中GPIO的配置
设备树是Linux描述硬件的“语言”。全志T113的GPIO引脚,在设备树里用gpios属性来描述。格式是:
gpios = <&gpio_bank pin_index flags>;
举个例子,控制一个LED:
leds {
compatible = "gpio-leds";
led_red {
label = "red-led";
gpios = <&pio 1 2 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
};
这里&pio是GPIO控制器的引用,1是bank号(PB组),2是引脚号(PB2),GPIO_ACTIVE_HIGH表示高电平点亮。
| 参数 | 说明 | 全志T113示例 |
|---|---|---|
| gpio_bank | GPIO组控制器节点引用 | &pio (PA/PB/PC...) |
| pin_index | 组内引脚编号,从0开始 | 2 表示该组第3个引脚 |
| flags | 电平极性:高有效/低有效 | GPIO_ACTIVE_HIGH / LOW |
4.3 使用GPIO子系统API控制LED
设备树配好了,驱动里怎么用?核心API就几个:
// 获取GPIO描述符
struct gpio_desc *gpiod_get(struct device *dev, const char *con_id,
enum gpiod_flags flags);
// 设置方向
int gpiod_direction_output(struct gpio_desc *desc, int value);
// 设置/读取电平
void gpiod_set_value(struct gpio_desc *desc, int value);
int gpiod_get_value(struct gpio_desc *desc);
// 释放
void gpiod_put(struct gpio_desc *desc);
写一个LED驱动的核心代码:
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
struct led_data {
struct gpio_desc *led_gpio;
};
static int led_probe(struct platform_device *pdev)
{
struct led_data *data;
struct device *dev = &pdev->dev;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 从设备树获取GPIO,con_id对应设备树里的"led-gpios"
data->led_gpio = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW);
if (IS_ERR(data->led_gpio)) {
dev_err(dev, "Failed to get LED GPIO\n");
return PTR_ERR(data->led_gpio);
}
// 设置输出方向,初始低电平
gpiod_direction_output(data->led_gpio, 0);
platform_set_drvdata(pdev, data);
return 0;
}
static int led_remove(struct platform_device *pdev)
{
// devm_* 系列会自动释放,这里可以留空
return 0;
}
static const struct of_device_id led_of_match[] = {
{ .compatible = "mycompany,led-driver" },
{ /* sentinel */ }
};
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);
你想想看,用devm_gpiod_get的好处是什么?自动管理生命周期,probe里申请,remove时自动释放。我早期写驱动都是手动gpiod_get/gpiod_put配对,后来发现devm_系列函数太香了,省心不少。
4.4 按键输入与中断驱动
按键检测有两种方式:轮询和中断。轮询浪费CPU,中断才是正经做法。全志T113的GPIO可以配置为中断源,设备树里这样配:
key {
compatible = "mycompany,key-driver";
key-gpios = <&pio 3 5 GPIO_ACTIVE_LOW>; // PD5,低电平表示按下
interrupts-extended = <&pio 3 5 IRQ_TYPE_EDGE_FALLING>;
};
驱动里申请中断的API:
// 将GPIO转为中断号
int gpiod_to_irq(const struct gpio_desc *desc);
// 申请中断
int devm_request_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, unsigned long flags,
const char *name, void *dev_id);
完整按键中断驱动示例:
struct key_data {
struct gpio_desc *key_gpio;
int irq;
};
static irqreturn_t key_irq_handler(int irq, void *dev_id)
{
struct key_data *data = dev_id;
int val;
// 读取当前电平,确认按键状态
val = gpiod_get_value(data->key_gpio);
if (val == 0) {
pr_info("Key pressed!\n");
} else {
pr_info("Key released!\n");
}
return IRQ_HANDLED;
}
static int key_probe(struct platform_device *pdev)
{
struct key_data *data;
struct device *dev = &pdev->dev;
int ret;
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
// 获取按键GPIO
data->key_gpio = devm_gpiod_get(dev, "key", GPIOD_IN);
if (IS_ERR(data->key_gpio)) {
dev_err(dev, "Failed to get key GPIO\n");
return PTR_ERR(data->key_gpio);
}
// 设置为输入
gpiod_direction_input(data->key_gpio);
// 获取中断号
data->irq = gpiod_to_irq(data->key_gpio);
if (data->irq < 0) {
dev_err(dev, "Failed to get IRQ\n");
return data->irq;
}
// 申请中断,下降沿触发
ret = devm_request_irq(dev, data->irq, key_irq_handler,
IRQF_TRIGGER_FALLING, "my-key", data);
if (ret) {
dev_err(dev, "Failed to request IRQ\n");
return ret;
}
dev_info(dev, "Key driver loaded, IRQ=%d\n", data->irq);
platform_set_drvdata(pdev, data);
return 0;
}
static const struct of_device_id key_of_match[] = {
{ .compatible = "mycompany,key-driver" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, key_of_match);
static struct platform_driver key_driver = {
.probe = key_probe,
.remove = key_remove,
.driver = {
.name = "my-key",
.of_match_table = key_of_match,
},
};
module_platform_driver(key_driver);
4.5 避坑指南:我踩过的那些坑
- GPIO复用冲突:全志T113的引脚功能很多,同一个引脚可能是GPIO,也可能是UART或I2C。设备树里配了GPIO,但pinctrl没配对,结果引脚没反应。检查
/sys/kernel/debug/gpio可以看当前GPIO状态。 - 中断触发方式:按键有抖动,下降沿触发可能一次按下触发多次中断。我一般加一个10ms的去抖定时器,或者用
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING双边沿触发,然后在软件里判断。 - 设备树编译:改完设备树记得编译成dtb,然后更新到板子上。我经常改了dts忘了编译,折腾半天发现还是旧配置。
- GPIO编号混淆:内核里GPIO编号是全局的,全志T113的PA0可能是0号,PB0可能是32号。用
gpiod_get按名字获取最安全,别自己算编号。
4.6 调试技巧
调试GPIO驱动,我常用的几个方法:
- 查看GPIO状态:
cat /sys/kernel/debug/gpio,能看到所有GPIO的申请者和方向。 - 手动操作GPIO:如果驱动还没写好,可以用
echo 32 > /sys/class/gpio/export先手动测试硬件是否正常。 - 测量波形:示波器或逻辑分析仪直接量引脚,看电平变化。我遇到过设备树配对了,但硬件虚焊的情况,示波器一量就发现了。
- 打印中断次数:在中断处理函数里加一个计数器,
cat /proc/interrupts看中断有没有触发。
cat /dev/mykey就能看到按键次数。
好了,GPIO子系统就讲到这里。从框架到设备树,从LED到按键中断,这些代码你都可以直接拿到全志T113上跑。记住,驱动开发的核心是“配好设备树,调好API”,剩下的就是耐心调试了。