第二章:GPIO驱动开发

各位同学,今天我们来聊聊GPIO驱动。说实话,GPIO是嵌入式开发里最基础、也最常用的外设之一。我刚开始做MTK平台时,第一个调试的外设就是GPIO。那时候踩了不少坑,今天我把这些经验都分享给你们。

2.1 GPIO硬件原理

GPIO,全称General Purpose Input Output,通用输入输出口。说白了,就是芯片上那些可以编程控制的引脚。你可以让它输出高电平或低电平,也可以读取外部信号的电平状态。

在MTK芯片内部,每个GPIO引脚都连接着多个功能模块。比如一个引脚,既可以当普通GPIO用,也可以配置成UART的TX、I2C的SCL,或者PWM的输出。这就是所谓的「引脚复用」。我在项目中遇到过,有同事把本该当I2C用的引脚配成了GPIO,结果传感器死活读不到数据。嗯,这种问题排查起来很头疼。

GPIO的硬件结构其实不复杂,核心就三个部分:

  • 输出驱动器:控制引脚输出高/低电平
  • 输入缓冲器:读取引脚上的电平状态
  • 上拉/下拉电阻:引脚悬空时保持确定电平

你想想看,如果引脚悬空,电平是不确定的。这时候就需要内部上拉或下拉电阻来固定电平。我建议你在设计电路时,尽量使用外部上拉电阻,因为内部电阻的精度和驱动能力都有限。

2.2 MTK GPIO控制器寄存器

MTK的GPIO控制器,说白了就是一堆寄存器。每个GPIO引脚对应几个关键的寄存器位。我整理了一下,常用的寄存器有这些:

寄存器名称 功能描述 位宽
GPIO_MODE 引脚功能选择(GPIO/复用功能) 3-4位
GPIO_DIR 方向控制(输入/输出) 1位
GPIO_DOUT 输出数据寄存器 1位
GPIO_DIN 输入数据寄存器(只读) 1位
GPIO_PULL_EN 上拉/下拉使能 1位
GPIO_PULL_SEL 上拉/下拉选择 1位

这里要注意,不同MTK芯片的寄存器偏移地址可能不一样。比如MT6765和MT6785,GPIO基地址就不同。我习惯在调试时先看芯片手册的「GPIO Register Map」章节,确认基地址和偏移量。

我曾经踩过的坑: 配置GPIO_MODE寄存器时,一定要先读后写。因为有些位是保留位,直接写会破坏其他引脚的配置。我见过有人直接写0xFFFFFFFF,结果整个GPIO控制器都乱了。

2.3 DTS配置

在MTK平台上,GPIO的配置主要通过设备树(DTS)来完成。DTS文件里,每个GPIO引脚都有对应的节点。我给你们看一个典型的配置:

/* 在dts文件中配置GPIO */
&pio {
    /* 定义GPIO引脚 */
    gpio_led_pins: gpio_led {
        pins_cmd_dat {
            pinmux = <MTK_PINMUX(GPIO10, 0)>;  /* GPIO10,功能0即GPIO模式 */
            slew-rate = <1>;                     /* 1:输出,0:输入 */
            bias-pull-up;                        /* 内部上拉 */
            drive-strength = <4>;               /* 驱动能力,单位mA */
        };
    };
};

/* 在设备节点中引用 */
led_device: led {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&gpio_led_pins>;
    pinctrl-1 = <&gpio_led_sleep>;
};

这里有几个关键点:

  • pinmux:指定引脚号和功能模式。0表示纯GPIO模式
  • slew-rate:控制方向。1是输出,0是输入
  • bias-pull-up/down:内部上拉或下拉
  • drive-strength:驱动能力。我一般设4mA,除非有特殊需求

我个人习惯把不同状态的引脚配置分开定义。比如「default」状态和「sleep」状态,这样在系统休眠时可以自动切换引脚配置,省电又安全。

小技巧: 调试DTS配置时,可以查看/sys/kernel/debug/pinctrl/下的文件。里面会显示每个引脚的当前配置状态,非常方便。

2.4 GPIO操作API

在驱动代码里,我们通过内核提供的API来操作GPIO。MTK平台完全兼容标准的Linux GPIO子系统。常用的API有这些:

2.4.1 gpio_request

使用GPIO前,必须先申请。就像你住酒店要先登记一样:

int gpio_request(unsigned gpio, const char *label);

/* 示例 */
int ret;
ret = gpio_request(10, "my_led");
if (ret < 0) {
    pr_err("Failed to request GPIO10: %d\n", ret);
    return ret;
}

这里要注意,label参数最好起个有意义的名字。我在项目中遇到过,系统里同时有多个驱动使用GPIO,如果label都是"gpio",排查冲突时根本分不清是谁在用。

2.4.2 gpio_set_value

设置GPIO输出电平:

void gpio_set_value(unsigned gpio, int value);

/* 示例:点亮LED */
gpio_set_value(10, 1);  /* 输出高电平 */
mdelay(100);            /* 延时100ms */
gpio_set_value(10, 0);  /* 输出低电平 */

你想想看,如果GPIO配置成了输入模式,调用gpio_set_value会怎样?嗯,不会有任何效果,但也不会报错。所以一定要先确认方向。

2.4.3 gpio_get_value

读取GPIO输入电平:

int gpio_get_value(unsigned gpio);

/* 示例:读取按键状态 */
int val = gpio_get_value(20);
if (val == 0) {
    pr_info("Button pressed\n");
}

2.4.4 gpio_direction_output / gpio_direction_input

设置GPIO方向:

int gpio_direction_output(unsigned gpio, int value);
int gpio_direction_input(unsigned gpio);

/* 示例 */
gpio_direction_output(10, 0);  /* 设置为输出,初始低电平 */
gpio_direction_input(20);      /* 设置为输入 */
重要提醒: gpio_direction_output的第二个参数,会在设置方向的同时输出指定电平。这样可以避免引脚电平跳变。我建议你在初始化时就用这个函数,而不是先设方向再设电平。

2.4.5 gpio_free

使用完后释放GPIO:

void gpio_free(unsigned gpio);

/* 示例 */
gpio_free(10);

释放GPIO这个操作,很多人会忘记。尤其是在驱动卸载时,如果不释放,下次加载驱动就会申请失败。我曾经调试过一个bug,就是驱动反复加载卸载,最后GPIO资源耗尽,系统直接panic了。

2.5 完整示例

最后,我给你们一个完整的GPIO驱动示例。这个例子控制一个LED闪烁:

#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/delay.h>

#define LED_GPIO 10

static int __init led_init(void)
{
    int ret;

    /* 申请GPIO */
    ret = gpio_request(LED_GPIO, "led_gpio");
    if (ret) {
        pr_err("gpio_request failed: %d\n", ret);
        return ret;
    }

    /* 设置为输出,初始低电平 */
    gpio_direction_output(LED_GPIO, 0);

    /* 闪烁3次 */
    for (int i = 0; i < 3; i++) {
        gpio_set_value(LED_GPIO, 1);
        mdelay(500);
        gpio_set_value(LED_GPIO, 0);
        mdelay(500);
    }

    return 0;
}

static void __exit led_exit(void)
{
    /* 关闭LED并释放GPIO */
    gpio_set_value(LED_GPIO, 0);
    gpio_free(LED_GPIO);
    pr_info("LED driver removed\n");
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("MTK GPIO LED Example");

这个例子虽然简单,但包含了GPIO开发的核心流程:申请、配置方向、读写、释放。你把这个流程吃透了,其他外设驱动开发也就触类旁通了。

好了,GPIO驱动开发就讲到这里。下一章我们讲UART驱动,那个比GPIO稍微复杂一点,但原理是相通的。有什么问题,欢迎在课程群里交流。