第三章 GPIO驱动开发:从寄存器到接口的实战之路

好,咱们今天来聊聊MTK8678平台上的GPIO驱动开发。说实话,GPIO这玩意儿看起来简单,不就是拉高拉低嘛?但实际做起来,坑还真不少。我当年刚接触MTK平台时,就被GPIO的复用功能折腾得够呛。今天我把这些经验掰开揉碎了讲给你听。

3.1 GPIO控制器寄存器映射

先说说寄存器映射。MTK8678的GPIO控制器,说白了就是一堆寄存器在控制。每个GPIO口都有对应的模式寄存器、数据寄存器、上拉下拉寄存器等等。

我个人习惯先把基地址搞清楚。在MTK8678上,GPIO控制器的基地址是 0x1000B000。嗯,这个地址你得记牢,后面所有操作都离不开它。

关键寄存器偏移量

寄存器名称 偏移地址 功能描述
GPIO_MODE 0x000 模式选择(GPIO/复用功能)
GPIO_DOUT 0x100 数据输出寄存器
GPIO_DIN 0x200 数据输入寄存器
GPIO_PULLEN 0x300 上拉/下拉使能
GPIO_PULLSEL 0x400 上拉/下拉选择

你想想看,每个GPIO口占几位?MTK8678每个寄存器管理32个GPIO,所以每个GPIO占1位。但模式寄存器不一样,它每个GPIO占3位,用来选择不同的复用功能。

/* GPIO寄存器映射结构体 */
struct mtk_gpio_regs {
    volatile uint32_t mode[8];      /* 8个寄存器,每个管理4个GPIO的模式 */
    volatile uint32_t dout[8];      /* 数据输出 */
    volatile uint32_t din[8];       /* 数据输入 */
    volatile uint32_t pullen[8];    /* 上拉使能 */
    volatile uint32_t pullsel[8];   /* 上拉选择 */
};

我的经验: 映射寄存器时一定要用 volatile 关键字。我曾经在一个项目里忘了加,结果编译器优化后,读到的永远是缓存值,排查了整整两天才找到问题。

3.2 DTS配置:设备树的正确打开方式

DTS配置这块,很多新手容易搞混。MTK8678的设备树里,GPIO节点长这样:

&pio {
    /* 定义GPIO别名 */
    gpio_led: gpio@10 {
        label = "led_green";
        gpios = <&pio 10 0>;
        /* 0表示输出,1表示输入 */
    };

    gpio_key: gpio@20 {
        label = "power_key";
        gpios = <&pio 20 1>;
        /* 1表示输入,带内部上拉 */
        input;
        debounce = <10000>;  /* 10ms消抖 */
    };
};

这里有个坑,我必须要说。DTS里的 gpios 属性,第二个参数是GPIO号,第三个参数是标志位。标志位可不是随便写的,它决定了驱动初始化的行为。

注意: MTK8678的GPIO号不是连续的!GPIO10和GPIO20之间可能隔着其他外设的引脚。我曾经在配置I2C的GPIO时,以为GPIO号是顺序的,结果死活调不通。后来查了芯片手册才发现,中间有几个GPIO被内部模块占用了。

我个人建议,在DTS里给每个GPIO起个有意义的别名。比如 led_greenpower_key,这样后续维护时一看就懂。别学某些人,全用 gpio_0gpio_1 这种名字,三个月后自己都忘了是干嘛的。

3.3 GPIO操作接口实现

好了,到了最核心的部分——写代码。MTK平台提供了标准的GPIO操作接口,但咱们得理解底层是怎么实现的。

3.3.1 初始化接口

static int mtk_gpio_request(struct gpio_chip *chip, unsigned int offset)
{
    struct mtk_gpio_bank *bank = gpiochip_get_data(chip);
    unsigned long flags;

    spin_lock_irqsave(&bank->lock, flags);
    
    /* 检查GPIO是否已被占用 */
    if (bank->reserved_map & BIT(offset)) {
        spin_unlock_irqrestore(&bank->lock, flags);
        return -EBUSY;
    }
    
    /* 标记为已占用 */
    bank->reserved_map |= BIT(offset);
    
    spin_unlock_irqrestore(&bank->lock, flags);
    return 0;
}

为什么要有 spin_lock_irqsave?你想想看,GPIO操作可能在中断上下文被调用,不加锁的话,并发访问会出大问题。我在做某个项目时,就因为忘了加锁,导致两个线程同时操作同一个GPIO,结果输出波形完全乱掉了。

3.3.2 方向设置与数据读写

static int mtk_gpio_direction_output(struct gpio_chip *chip, 
                                     unsigned int offset, int value)
{
    struct mtk_gpio_bank *bank = gpiochip_get_data(chip);
    void __iomem *base = bank->base;
    uint32_t reg;
    
    /* 先写输出值,再设方向 —— 这个顺序很重要 */
    reg = readl(base + GPIO_DOUT_OFFSET);
    if (value)
        reg |= BIT(offset);
    else
        reg &= ~BIT(offset);
    writel(reg, base + GPIO_DOUT_OFFSET);
    
    /* 设置方向为输出 */
    reg = readl(base + GPIO_DIR_OFFSET);
    reg |= BIT(offset);
    writel(reg, base + GPIO_DIR_OFFSET);
    
    return 0;
}

避坑指南: 我曾经犯过一个错误——先设方向再写值。结果在GPIO从输入切换到输出时,会瞬间输出一个不确定的电平。如果这个GPIO控制的是电机或者继电器,那后果...嗯,你懂的。所以一定要先写值,再设方向。

3.3.3 中断配置

GPIO中断这块,MTK8678支持多种触发方式:上升沿、下降沿、高电平、低电平。配置代码如下:

static int mtk_gpio_set_irq_type(struct irq_data *data, unsigned int type)
{
    struct mtk_gpio_bank *bank = irq_data_get_irq_chip_data(data);
    unsigned int gpio = data->hwirq;
    void __iomem *base = bank->base;
    uint32_t reg;
    
    /* 配置触发类型 */
    reg = readl(base + GPIO_INT_TYPE_OFFSET);
    switch (type) {
    case IRQ_TYPE_EDGE_RISING:
        reg |= BIT(gpio);           /* 边沿触发 */
        writel(reg, base + GPIO_INT_TYPE_OFFSET);
        reg = readl(base + GPIO_INT_POLARITY_OFFSET);
        reg |= BIT(gpio);           /* 上升沿 */
        writel(reg, base + GPIO_INT_POLARITY_OFFSET);
        break;
    case IRQ_TYPE_EDGE_FALLING:
        reg |= BIT(gpio);
        writel(reg, base + GPIO_INT_TYPE_OFFSET);
        reg = readl(base + GPIO_INT_POLARITY_OFFSET);
        reg &= ~BIT(gpio);          /* 下降沿 */
        writel(reg, base + GPIO_INT_POLARITY_OFFSET);
        break;
    /* 其他类型类似... */
    }
    
    return 0;
}

我的建议: 调试GPIO中断时,先用示波器看看引脚上实际的波形。很多时候不是代码问题,而是硬件上信号有毛刺。我曾经遇到一个案子,按键按下时波形抖动严重,最后加了硬件消抖电路才解决。

3.4 实战中的那些坑

最后,我总结几个实际项目中容易踩的坑:

  1. GPIO复用冲突:同一个引脚可能被多个模块使用。比如GPIO20既是UART的TX,又是PWM的输出。配置前一定要确认当前引脚没有被其他驱动占用。
  2. 休眠唤醒问题:系统进入休眠后,GPIO的状态可能会丢失。我建议在 suspend 回调里保存GPIO状态,在 resume 里恢复。
  3. 电平转换:MTK8678的GPIO电压是1.8V,如果外接3.3V的设备,必须加电平转换芯片。别想着直接连,会烧芯片的。
  4. 驱动加载顺序:如果GPIO驱动依赖其他驱动(比如时钟、电源域),一定要在DTS里配置好 depends-on 属性。

嗯,GPIO驱动开发大概就这些内容。说白了,就是寄存器操作加设备树配置,但细节决定成败。下一章咱们聊聊PWM驱动,那个更有意思,涉及到定时器和波形生成。到时候我会分享一个我做呼吸灯时踩过的坑,保证让你印象深刻。