3. GPIO驱动开发:从控制器到中断实战

GPIO驱动,说白了就是让芯片的引脚听话——该输出时输出,该输入时输入,该触发中断时别掉链子。我在鸿蒙南向开发中接触过不少GPIO控制器,各家芯片的寄存器布局千奇百怪,但核心逻辑就那么几块。今天咱们就把GPIO驱动拆开揉碎,从控制器驱动讲到按键中断实战。

3.1 GPIO控制器驱动:管脚的总管家

每个GPIO控制器都管着一组引脚。控制器驱动要干三件事:

  • 管脚复用配置——这个脚是当GPIO用,还是当I2C、UART用?
  • 电气特性设置——上拉、下拉、开漏、推挽、驱动能力。
  • 中断路由——哪个引脚能触发中断,触发边沿是什么。

我个人习惯把控制器驱动分成两层:

层次 职责 典型接口
HAL层 屏蔽寄存器差异,提供统一操作结构体 GpioSetDir(), GpioWrite()
Kernel层 直接操作寄存器,处理时钟和电源 writel(), readl()

嗯,这里要注意:鸿蒙的GPIO驱动框架在//drivers/hdf_core/framework/support/platform/gpio下,它定义了一套标准操作集。你写控制器驱动时,只需要填充这个结构体:

struct GpioMethod {
    int32_t (*request)(struct GpioCntlr *cntlr, uint16_t gpio);
    int32_t (*release)(struct GpioCntlr *cntlr, uint16_t gpio);
    int32_t (*write)(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t val);
    int32_t (*read)(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t *val);
    int32_t (*setDir)(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t dir);
    int32_t (*getDir)(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t *dir);
    int32_t (*toIrq)(struct GpioCntlr *cntlr, uint16_t gpio);
    int32_t (*setIrq)(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t mode);
    int32_t (*unsetIrq)(struct GpioCntlr *cntlr, uint16_t gpio);
    int32_t (*enableIrq)(struct GpioCntlr *cntlr, uint16_t gpio);
    int32_t (*disableIrq)(struct GpioCntlr *cntlr, uint16_t gpio);
};
我的经验:写setDir时,记得先读后写。有些芯片的GPIO方向寄存器和数据寄存器共用同一个地址,直接写会把数据搞乱。我曾经在这个坑里爬了整整一个下午。

3.2 GPIO操作接口:用户态怎么用?

控制器驱动写好了,上层应用怎么操作GPIO?鸿蒙提供了两组接口:

  • 内核态接口:GpioCntlrWrite(), GpioCntlrRead() 等,驱动模块内部用。
  • 用户态接口:通过HDF框架暴露,应用层调用 GpioWrite(), GpioRead()。

你想想看,用户态调用一次GpioWrite(),背后发生了什么?

  1. 应用调用HDF接口,传入gpio编号和值。
  2. HDF框架根据gpio编号找到对应的控制器。
  3. 调用控制器驱动中的write方法。
  4. 驱动操作寄存器,引脚电平变化。

整个过程不到微秒级,但每一步都有校验。我个人建议你在调试阶段,把GpioCntlrWrite里加个打印,看看传入的gpio编号是不是你预期的那个。

3.3 中断注册与处理:让引脚主动说话

轮询太浪费CPU了。真正干活的方式是中断——引脚电平一变,CPU立刻知道。

中断注册流程大致如下:

步骤 操作 说明
1 GpioCntlrToIrq() 把gpio编号转成中断号
2 request_irq() 注册中断处理函数
3 GpioCntlrSetIrq() 设置触发边沿(上升沿/下降沿/双沿)
4 GpioCntlrEnableIrq() 使能中断

中断处理函数里要做什么?记住一条铁律:快进快出。不要在中断上下文里做耗时操作,比如打印、内存分配、互斥锁等待。正确的做法是:

  • 读寄存器,确认中断源。
  • 清中断标志位。
  • 把数据塞进工作队列或tasklet。
  • 退出。
避坑指南:我曾经在中断处理函数里调了printk,结果系统直接死锁。为什么?因为printk可能触发另一个中断,而那个中断优先级更高,反过来又调了同一个GPIO控制器……死循环了。中断里别干多余的事。

3.4 实战:按键中断驱动

理论说完了,咱们动手写一个按键中断驱动。硬件连接很简单:按键一端接GPIO,另一端接地。按下时引脚变低,松开时变高。我们要实现:按下按键,打印一条日志。

先看设备树配置(以Hi3516为例):

gpio_keys {
    compatible = "hdf,gpio-key";
    gpios = <&gpio4 3 0>;  // GPIO4_3
    interrupt-parent = <&gpio4>;
    interrupts = <3 2>;     // 下降沿触发
    label = "user_key";
};

驱动代码核心部分:

#include "gpio_core.h"
#include "hdf_log.h"

#define KEY_GPIO_NUM   (4 * 8 + 3)  // GPIO4_3

static uint32_t g_irqNum = 0;

// 中断处理函数
static irqreturn_t KeyIrqHandler(int irq, void *devId)
{
    // 1. 读GPIO电平确认(防抖)
    uint16_t val;
    GpioCntlrRead(GetGpioCntlr(0), KEY_GPIO_NUM, &val);
    if (val == 1) {
        // 高电平,可能是抖动,忽略
        return IRQ_HANDLED;
    }

    // 2. 清中断标志(硬件相关)
    GpioCntlrUnsetIrq(GetGpioCntlr(0), KEY_GPIO_NUM);

    // 3. 上报事件(这里简化,直接打印)
    HDF_LOGI("Key pressed!");

    return IRQ_HANDLED;
}

int32_t KeyDriverInit(void)
{
    struct GpioCntlr *cntlr = GetGpioCntlr(0);
    if (cntlr == NULL) {
        return HDF_FAILURE;
    }

    // 设置GPIO为输入
    GpioCntlrSetDir(cntlr, KEY_GPIO_NUM, GPIO_DIR_IN);

    // 获取中断号
    g_irqNum = GpioCntlrToIrq(cntlr, KEY_GPIO_NUM);
    if (g_irqNum == 0) {
        return HDF_FAILURE;
    }

    // 注册中断
    if (request_irq(g_irqNum, KeyIrqHandler, IRQF_TRIGGER_FALLING,
                    "key_irq", NULL) != 0) {
        return HDF_FAILURE;
    }

    // 使能中断
    GpioCntlrSetIrq(cntlr, KEY_GPIO_NUM, GPIO_IRQ_TRIGGER_FALLING);
    GpioCntlrEnableIrq(cntlr, KEY_GPIO_NUM);

    HDF_LOGI("Key driver init success");
    return HDF_SUCCESS;
}
关键点:中断处理函数里先读电平再清标志,这个顺序不能反。如果先清标志再读电平,可能漏掉第二次按键。我调试时遇到过,按一次触发两次中断,就是因为清标志的时机不对。

实际项目中,按键驱动还要处理:

  • 软件防抖——用定时器延迟10~20ms再读电平。
  • 长按/短按区分——记录按下时间,超过阈值算长按。
  • 多按键矩阵——用行列扫描法,减少GPIO占用。

下面这张图展示了按键中断驱动的完整数据流:

按键中断驱动数据流 物理按键 GPIO控制器寄存器 GpioCntlrToIrq() request_irq() KeyIrqHandler() HDF_LOGI / 事件上报 说明: 按键按下 → GPIO电平变化 → 硬件触发中断 → 驱动调用已注册的处理函数 → 打印日志或上报事件

最后说一句:按键中断驱动看起来简单,但它是理解整个GPIO子系统的最佳入口。你把这个跑通了,后面I2C、SPI设备的中断驱动,套路都差不多。嗯,动手试试吧。