第4章:GPIO驱动实战——GPIO子系统框架、设备树GPIO配置、GPIO控制接口、实现LED驱动

各位同学,今天我们来啃一块硬骨头——GPIO驱动。说实话,GPIO在嵌入式开发里就像空气一样无处不在。你按个按键、点个灯、控制个继电器,背后都是GPIO在干活。但很多新手一上来就被设备树、子系统框架这些东西搞懵了。别急,我带你一步步捋清楚。

4.1 GPIO子系统框架:内核怎么管GPIO的?

先说说内核里的GPIO子系统。你想想看,一个SoC可能有几十上百个GPIO引脚,每个引脚还能复用成不同的功能。如果每个驱动都直接操作寄存器,那代码就乱成一锅粥了。所以内核搞了个GPIO子系统,统一管理这些引脚。

这个子系统分三层:

  • GPIO控制器驱动层:芯片厂商写的,负责操作具体寄存器。比如瑞芯微的pinctrl驱动。
  • GPIO核心层:内核提供的通用接口,屏蔽底层差异。
  • GPIO使用者层:我们写驱动时调用的API,比如gpiod_get_value()。

我画了个图,帮你理解这三层的关系:

GPIO子系统三层架构 用户层(使用者) LED驱动 / 按键驱动 / 其他外设驱动 核心层(GPIO Core) gpiod_get / gpiod_set_value / gpio_request 等API 控制器驱动层(GPIO Controller) pinctrl / gpio_chip 结构体实现 硬件层(RV1126 SoC GPIO寄存器) 调用关系

说白了,我们写驱动时只需要调用核心层提供的API,不用管底层寄存器怎么操作。内核会帮我们把请求路由到对应的GPIO控制器上。

我的经验:刚开始学GPIO驱动时,我总想直接操作寄存器,觉得那样更“底层”、更牛。后来发现用子系统API不仅代码简洁,而且移植性极好。换一个平台,只要改设备树就行,驱动代码几乎不用动。

4.2 设备树GPIO配置:怎么告诉内核引脚信息?

设备树是Linux下描述硬件信息的“说明书”。GPIO的配置主要涉及两个属性:

  • gpio-controller:标记这个节点是一个GPIO控制器。
  • #gpio-cells:指定引用GPIO时需要几个参数。通常是2个:引脚号和标志位。

来看一个RV1126设备树的实际例子:

// 在pinctrl节点中定义GPIO控制器
pinctrl: pinctrl {
    compatible = "rockchip,rk1126-pinctrl";
    // ... 其他属性
    
    gpio0: gpio0 {
        gpio-controller;
        #gpio-cells = <2>;
        // 中断相关配置
        interrupt-controller;
        #interrupt-cells = <2>;
    };
    
    gpio1: gpio1 {
        gpio-controller;
        #gpio-cells = <2>;
    };
};

// 在LED设备节点中引用GPIO
leds {
    compatible = "gpio-leds";
    
    status_led: led-0 {
        label = "status_led";
        gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_LOW>;
        linux,default-trigger = "heartbeat";
    };
};

这里有个细节要注意:GPIO_ACTIVE_LOW表示低电平有效。我遇到过有人把高电平有效和低电平有效搞反了,结果灯死活不亮,查了半天才发现是标志位写反了。

避坑指南:我曾经在一个项目里,设备树里GPIO引脚号写对了,但忘了加gpio-controller属性。结果内核根本不认这个节点,驱动probe都进不去。检查设备树时,一定要确认控制器节点有gpio-controller标志。

4.3 GPIO控制接口:内核提供了哪些API?

内核提供了两套GPIO控制接口:老式的gpio_xxx和新式的gpiod_xxx。我强烈建议你用新接口,它更安全、更现代。

常用的API如下:

函数 功能 说明
gpiod_get() 获取GPIO描述符 通过设备树中的名字获取
gpiod_direction_output() 设置GPIO为输出 同时设置初始电平
gpiod_direction_input() 设置GPIO为输入 读取引脚电平前调用
gpiod_set_value() 设置GPIO输出电平 0或1
gpiod_get_value() 读取GPIO输入电平 返回0或1
gpiod_put() 释放GPIO描述符 驱动卸载时调用

这些API用起来很简单,但有个坑:gpiod_get()的第二个参数是GPIO的名字,必须和设备树里的label属性对应上。我刚开始用的时候,名字写错了,结果返回NULL,驱动直接崩了。

4.4 实现LED驱动:从零写一个完整的例子

好了,理论知识讲完了,我们来写一个真正的LED驱动。这个驱动会控制RV1126开发板上的一个LED,让它以1秒为周期闪烁。

先看设备树配置:

// rv1126-evb.dts 中添加
/ {
    my_led: my-led {
        compatible = "my-led-driver";
        led-gpios = <&gpio1 RK_PB2 GPIO_ACTIVE_LOW>;
        status = "okay";
    };
};

然后是驱动代码:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/delay.h>
#include <linux/timer.h>

struct my_led_data {
    struct gpio_desc *led_gpio;
    struct timer_list timer;
    int state;
};

static void led_timer_callback(struct timer_list *t)
{
    struct my_led_data *data = from_timer(data, t, timer);
    
    // 翻转LED状态
    data->state = !data->state;
    gpiod_set_value(data->led_gpio, data->state);
    
    // 重新设置定时器,1秒后再次触发
    mod_timer(&data->timer, jiffies + msecs_to_jiffies(1000));
}

static int my_led_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_led_data *data;
    int ret;
    
    // 分配私有数据
    data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    
    // 获取GPIO描述符
    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);
    }
    
    // 初始化定时器
    timer_setup(&data->timer, led_timer_callback, 0);
    data->state = 0;
    
    // 启动定时器
    mod_timer(&data->timer, jiffies + msecs_to_jiffies(1000));
    
    platform_set_drvdata(pdev, data);
    dev_info(dev, "LED driver probed successfully\n");
    
    return 0;
}

static int my_led_remove(struct platform_device *pdev)
{
    struct my_led_data *data = platform_get_drvdata(pdev);
    
    // 删除定时器
    del_timer_sync(&data->timer);
    
    // 关闭LED
    gpiod_set_value(data->led_gpio, 0);
    
    dev_info(&pdev->dev, "LED driver removed\n");
    
    return 0;
}

static const struct of_device_id my_led_of_match[] = {
    { .compatible = "my-led-driver" },
    { }
};
MODULE_DEVICE_TABLE(of, my_led_of_match);

static struct platform_driver my_led_driver = {
    .probe = my_led_probe,
    .remove = my_led_remove,
    .driver = {
        .name = "my_led",
        .of_match_table = my_led_of_match,
    },
};

module_platform_driver(my_led_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple LED driver for RV1126");

代码看着不长,但每个细节都有讲究。我重点说几个地方:

  • devm_gpiod_get():带devm_前缀的函数会自动管理资源,驱动卸载时自动释放GPIO。省心!
  • timer_setup():内核新版本的定时器初始化方式。老版本用init_timer(),但新内核已经废弃了。
  • mod_timer():修改定时器的超时时间。这里用来实现周期性闪烁。
核心要点:写GPIO驱动时,记住三步走:设备树描述引脚 → probe中获取GPIO → 用API控制电平。这三步走对了,驱动就基本成型了。

编译这个驱动,生成.ko文件,然后加载到开发板上:

# 编译
make -C /path/to/kernel M=$(pwd) modules

# 加载
insmod my_led.ko

# 查看内核日志
dmesg | tail

# 卸载
rmmod my_led

如果一切正常,你会看到LED以1秒为周期闪烁。嗯,那种感觉还是挺有成就感的。

我的习惯:调试GPIO驱动时,我喜欢先用gpioinfo命令查看GPIO的状态。比如gpioinfo gpiochip0可以列出所有引脚的方向和电平。这比看代码快多了。

最后说一句,GPIO驱动是嵌入式Linux驱动开发的入门砖。你把这个搞透了,后面学I2C、SPI、PWM这些子系统就会轻松很多。因为它们的设计思路是相通的——设备树描述硬件,子系统提供API,驱动开发者只管调用。

好了,这一章的内容就到这儿。代码我已经测试过了,在RV1126上跑得很稳。你照着写一遍,遇到问题欢迎交流。


可可精品资料库