4. GPIO驱动与板级适配

在 TI Sitara(AM335x/AM437x/AM57xx 等)平台上,GPIO 是最基础也最容易被低估的外设。它既可能只是一个点灯的输出脚,也可能承担按键输入、外部中断源、复位控制、电源使能等关键角色。要把它用稳,需要同时理解三层内容:SoC 内部的 GPIO 控制器结构、Linux gpio 子系统提供的 API 抽象,以及设备树如何把“第几个 bank 的第几号引脚”翻译成驱动可用的资源。

一、Sitara GPIO 控制器结构

以 AM335x 为例,芯片内部集成 4 个 GPIO 模块(GPIO0~GPIO3),每个模块管理 32 条引脚,共 128 条 GPIO。每条引脚在控制器内部对应一个数据位,寄存器组按 bank 组织,典型寄存器包括:

  • GPIO_OE:方向寄存器,某位为 1 表示输出,为 0 表示输入(注意与直觉相反,这是 Sitara 的约定)。
  • GPIO_DATAIN:只读,反映引脚当前电平。
  • GPIO_DATAOUT:输出数据寄存器,写入即改变输出电平。
  • GPIO_LEVELDETECT0/1、GPIO_RISINGDETECT、GPIO_FALLINGDETECT:中断触发方式配置。
  • GPIO_IRQSTATUS_0/1、GPIO_IRQSTATUS_SET_0/1、GPIO_IRQSTATUS_CLR_0/1:中断状态与使能/清除。

每个 bank 的中断输出会汇聚到 GIC(Cortex-A8 的中断控制器),在 AM335x 上通常映射为 IRQ 96~127 这一段。也就是说,GPIO0 的 32 条引脚共享一个 bank 级中断线,进入中断服务程序后必须通过 IRQSTATUS 判断具体是哪一位触发,再分发给对应的处理逻辑。这一点是理解后续“中断映射”和“共享中断”的关键。

Sitara GPIO 控制器与中断汇聚结构(以 AM335x 为例) GPIO0 32 pins · OE/DATAIN/DATAOUT GPIO1 32 pins · 独立寄存器组 GPIO2 32 pins · 独立寄存器组 GPIO3 32 pins IRQ 96 IRQ 97 IRQ 98 IRQ 99 GIC 中断控制器 bank 级共享 IRQSTATUS 判位 再分发到具体引脚 Cortex-A8 IRQ 处理入口 Pin Mux(引脚复用) GPIO0_20 → 按键 GPIO1_28 → LED 外部引脚

二、gpio 子系统 API

Linux 内核的 gpio 子系统把不同 SoC 的 GPIO 控制器统一抽象为 struct gpio_chip,驱动开发者通常不直接操作寄存器,而是使用以下 API:

int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);
int gpio_get_value(unsigned gpio);
void gpio_set_value(unsigned gpio, int value);
int gpio_to_irq(unsigned gpio);

其中 gpio_to_irq() 把某个 GPIO 编号转换成对应的 Linux 虚拟中断号,这是按键中断驱动中最常用的一步。较新的内核推荐使用描述符式 API(gpiod_get()、gpiod_get_value()、gpiod_to_irq()),它天然与设备树绑定,能避免全局 GPIO 编号冲突。在 Sitara 上,GPIO 编号通常按 bank * 32 + pin 计算,例如 GPIO1_28 对应编号 60。

三、中断映射与设备树配置

设备树是板级适配的核心。一个典型的按键节点如下:

gpio_keys {
    compatible = "gpio-keys";
    pinctrl-names = "default";
    pinctrl-0 = &key_pins;

    button0 {
        label = "user-key";
        gpios = <&gpio1 28 GPIO_ACTIVE_LOW>;
        linux,code = <KEY_ENTER>;
        interrupt-parent = <&gpio1>;
        interrupts = <28 IRQ_TYPE_EDGE_FALLING>;
    };
};

这里 interrupt-parent 指向 GPIO1 控制器,interrupts 的第一个单元格是 bank 内引脚号,第二个是触发类型。GPIO_ACTIVE_LOW 表示低电平有效,与按键按下接地一致。LED 节点则更简单,通常用 gpios = <&gpio1 28 GPIO_ACTIVE_HIGH> 配合 gpio-leds 驱动即可。

四、按键与 LED 驱动实战

下面给出一个最小可用的按键中断驱动骨架,用于演示申请 GPIO、映射中断、注册处理函数和防抖的完整链路:

static irqreturn_t key_isr(int irq, void *dev_id)
{
    struct key_dev *k = dev_id;
    unsigned long flags;

    spin_lock_irqsave(&k->lock, flags);
    if (time_before(jiffies, k->last_jiffies + msecs_to_jiffies(20))) {
        spin_unlock_irqrestore(&k->lock, flags);
        return IRQ_HANDLED;   /* 20ms 内重复触发,判为抖动 */
    }
    k->last_jiffies = jiffies;
    k->press_count++;
    spin_unlock_irqrestore(&k->lock, flags);

    schedule_work(&k->work);   /* 下半部处理,避免中断上下文耗时 */
    return IRQ_HANDLED;
}

这段代码体现了两个工程要点:第一,用 jiffies 做时间窗过滤,20ms 内的重复中断直接丢弃,这是最轻量的软件防抖;第二,中断上下文只做计数和调度,真正的按键事件上报放到工作队列中完成,避免在中断里调用可能睡眠的函数。

五、防抖与中断处理策略

机械按键在按下和释放瞬间会产生 5~20ms 的抖动,表现为电平快速跳变。处理方式有三类:

  • 硬件防抖:在按键两端并联 100nF 电容,或使用 RC 滤波,成本低但会改变边沿速度。
  • 软件时间窗:如上面的 jiffies 判断,实现简单,适合中断触发场景。
  • 定时器轮询:中断触发后启动 20ms 定时器,到期再采样电平,确认稳定后才上报,抗干扰更强。

在 Sitara 上还需要注意:GPIO bank 中断是共享的,如果同一 bank 上有多个中断源,ISR 中应先读 GPIO_IRQSTATUS_0/1 判断是哪一位触发,处理完再写 GPIO_IRQSTATUS_CLR 清除,否则会出现中断风暴。对于边沿触发,建议在设备树中明确写 IRQ_TYPE_EDGE_FALLING 或 IRQ_TYPE_EDGE_BOTH,不要依赖默认值。

调试建议:用 cat /proc/interrupts 观察 GPIO bank 中断计数是否随按键增长;用 gpioinfo 或 cat /sys/kernel/debug/gpio 确认引脚方向与电平;若中断不触发,优先检查 pinmux 是否已把引脚配置为 GPIO 模式,以及 interrupt-parent 是否指向正确的 bank。

把以上内容串起来,Sitara 的 GPIO 适配可以概括为一条清晰的链路:设备树描述引脚与中断属性 → pinctrl 完成复用配置 → gpio 子系统申请并映射中断 → 驱动在 ISR 中判位、防抖、调度下半部 → 用户空间通过 input 子系统或 sysfs 看到按键与 LED 的实际行为。掌握这条链路,后续的 I2C、SPI 等外设适配也会顺畅很多。