3、GPIO接口驱动开发:从控制器模型到按键中断实战

GPIO,说白了就是芯片的“手脚”。你要让传感器上报数据,或者让执行器干活,最终都得通过GPIO来操作。这一章,我们就来啃下这块硬骨头。

3.1 GPIO控制器模型:鸿蒙怎么管这些“手脚”

鸿蒙的GPIO驱动,不是让你直接去操作寄存器地址。它抽象了一套模型,叫GPIO控制器。我个人习惯,先把这个模型搞清楚,再写代码会顺手很多。

每个GPIO控制器,在鸿蒙里对应一个 GpioController 结构体。它管着一组GPIO管脚,比如一个GPIO0到GPIO31。控制器里会注册好操作函数:读、写、设置方向、配置中断。

你想想看,如果每个芯片都直接操作寄存器,那代码移植起来得多痛苦?鸿蒙这套模型,就是让你换芯片时,只换底层操作函数,上层逻辑纹丝不动。

核心概念: GPIO控制器 = 管脚集合 + 操作函数集。驱动开发者只需要实现 GpioController 里的回调,剩下的交给框架。

3.2 GPIO管脚配置:别小看这一步

配置管脚,看似简单,坑却不少。我记得刚做驱动时,有个传感器死活读不到数据,查了两天,最后发现是管脚复用没配对。

鸿蒙里配置一个GPIO管脚,通常需要这几步:

  1. 获取控制器句柄:通过设备树或平台数据拿到 GpioController。
  2. 设置方向:输入还是输出?用 GpioSetDir()。
  3. 设置输出值:如果是输出,设高或低。
  4. 配置上下拉:有些场景需要内部上拉或下拉。
  5. 配置驱动能力:电流驱动强度,一般默认就行。
配置项 API函数 说明
设置方向 GpioSetDir(gpio, dir) dir: GPIO_DIR_IN / GPIO_DIR_OUT
读取电平 GpioRead(gpio, &val) val: 0 或 1
写入电平 GpioWrite(gpio, val) val: GPIO_VAL_LOW / GPIO_VAL_HIGH
设置中断触发 GpioSetIrq(gpio, mode, func, arg) mode: 上升沿、下降沿、双边沿、高电平、低电平
我的经验: 配置管脚时,一定要先确认这个管脚有没有被其他外设占用。我曾经在调试I2C时,发现SCL管脚被GPIO模块抢了,导致总线一直拉低。用 cat /sys/kernel/debug/gpio 可以快速查看管脚状态。

3.3 中断注册与响应:让CPU别傻等

轮询?那是下下策。真正高效的驱动,都是用中断。按键按下,CPU才来处理,平时该干嘛干嘛。

鸿蒙的中断注册流程,我总结为三步:

  1. 申请中断号:通过 GpioToIrq(gpio) 把GPIO管脚映射成中断号。
  2. 注册中断处理函数:调用 request_irq(irq, handler, flags, name, dev)。
  3. 使能中断:调用 enable_irq(irq)。

中断处理函数里,有个铁律:快进快出。不要在中断上下文里做耗时操作,比如打印、内存分配。真要处理数据,用工作队列或tasklet推到下半部去。

避坑指南: 我曾经在中断处理函数里直接调了 printk,结果系统直接卡死。原因是printk本身可能引起调度,而中断上下文不允许调度。记住,中断里只做标记,别干重活。

3.4 实战:按键中断驱动

光说不练假把式。我们写一个真实的按键中断驱动。假设按键接在GPIO5,按下为低电平。

先看代码框架:

#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/of.h>

static int key_gpio = 5;
static int key_irq;
static struct work_struct key_work;

// 工作队列处理函数(下半部)
static void key_work_handler(struct work_struct *work)
{
    int val;
    gpio_get_value(key_gpio, &val);
    printk("Key pressed! value = %d\n", val);
    // 这里可以上报事件给上层
}

// 中断上半部
static irqreturn_t key_isr(int irq, void *dev_id)
{
    // 调度下半部
    schedule_work(&key_work);
    return IRQ_HANDLED;
}

static int __init key_drv_init(void)
{
    int ret;

    // 1. 申请GPIO
    ret = gpio_request(key_gpio, "key-gpio");
    if (ret) {
        printk("gpio request failed\n");
        return ret;
    }

    // 2. 设置方向为输入
    gpio_direction_input(key_gpio);

    // 3. 映射中断号
    key_irq = gpio_to_irq(key_gpio);

    // 4. 注册中断,下降沿触发
    ret = request_irq(key_irq, key_isr, IRQF_TRIGGER_FALLING,
                      "key_irq", NULL);
    if (ret) {
        printk("request irq failed\n");
        gpio_free(key_gpio);
        return ret;
    }

    // 5. 初始化工作队列
    INIT_WORK(&key_work, key_work_handler);

    printk("key driver init ok\n");
    return 0;
}

static void __exit key_drv_exit(void)
{
    free_irq(key_irq, NULL);
    gpio_free(key_gpio);
    printk("key driver exit\n");
}

module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");

这段代码,我拆开讲几个关键点:

  • gpio_to_irq:把物理GPIO转成系统中断号,不同平台映射方式不同,但API统一了。
  • IRQF_TRIGGER_FALLING:下降沿触发,按键按下时电平从高变低。
  • schedule_work:把耗时操作推到进程上下文,避免中断里卡死。
调试技巧: 加载驱动后,用 cat /proc/interrupts 查看中断是否注册成功。如果按键按下,中断计数应该增加。如果没反应,先量一下管脚电平,确认硬件连接没问题。

嗯,这里要注意,工作队列里的 printk 只是演示。实际产品中,你应该用 input_report_key 上报给输入子系统,或者通过netlink发消息给用户态。

GPIO中断驱动流程 按键按下(硬件事件) GPIO电平变化 中断控制器响应 中断上半部(ISR) 调度工作队列(下半部) 上报事件 / 处理数据 关键说明 • 中断上半部必须快 • 下半部处理耗时操作 • 避免在ISR中调用printk 调试方法 • cat /proc/interrupts • 示波器量管脚电平

这张图把整个中断响应链路画清楚了。从硬件按键按下,到最终事件上报,每一步都有明确的职责。你写驱动时,脑子里要有这张图,就知道代码该往哪个环节塞。

个人建议: 刚开始学中断驱动,别急着写复杂逻辑。先让中断处理函数里只打印一句话,确认中断能触发。然后再慢慢加工作队列、上报事件。一步一个脚印,出问题了也好定位。