本文是个人项目记录(七)GPIO和中断子系统控制器驱动:基于i.MX6ULL的嵌入式Linux终端系统构建与多子系统控制器驱动开发—GPIO和中断控制器驱动部分的完整开发记录。
这是项目的最后一部分,记录基于i.MX6ULL平台,从零编写GPIO控制器驱动(gpio_chip)同时也是子中断控制器驱动(irq_chip + irq_domain + chained handler),GPIO功能通过sysfs接口控制LED验证,中断功能通过一个极简的测试设备驱动+SR501人体红外模块验证。调试使用Ftrace的trace_event和function_graph追踪中断处理的完整函数调用链


项目已完成,开源在我的Github,有完整的文档和源码,欢迎Star⭐
仓库地址:[https://github.com/illusOwo-ff/i.MX6ULL-bsp-driver-project]

一、本部分说明

本部分是"基于i.MX6ULL的嵌入式Linux终端系统构建与多子系统控制器驱动开发"项目的最后一个驱动部分。前面已完成UART控制器驱动、I2C适配器驱动、SPI控制器驱动,本模块实现GPIO控制器+中断控制器。

之所以GPIO控制器同时也是子中断控制器的原因是SPI/I2C/UART的中断是直连GIC的单根线,驱动里直接devm_request_irq就行。GPIO不同,每组32个引脚共享2根GIC中断线(pin0-15一根,pin16-31一根),需要一个子中断控制器来读ISR寄存器分辨具体是哪个引脚触发的,并且由于GPIO的中断功能寄存器和GPIO功能寄存器在同一块硬件空间,因此和官方一样,GPIO控制器驱动里同时实现了gpio_chip(GPIO功能)和irq_chip+irq_domain(中断控制器功能)。


二、关键结构体与回调函数

GPIO部分——gpio_chip

gpio_chip是GPIO控制器的核心结构体,描述一组GPIO引脚的操作方法

需要设置的成员:

成员 填什么 说明
label 自定义名字 在/sys/class/gpio/gpiochipN/label里显示
parent &pdev->dev 关联platform_device
owner THIS_MODULE 固定
base -1 动态分配GPIO全局编号
ngpio 32 一组GPIO有32个引脚
of_node pdev->dev.of_node 让内核通过设备树找到此GPIO控制器
can_sleep false MMIO操作不会睡眠
direction_input 自己实现 操作GDIR寄存器
direction_output 自己实现 操作DR和GDIR寄存器
get 自己实现 读PSR寄存器
set 自己实现 写DR寄存器
get_direction 自己实现 读GDIR寄存器
request gpiochip_generic_request 内核函数,转发给pinctrl申请引脚复用
free gpiochip_generic_free 内核函数,释放pinctrl占用
to_irq 自己实现 调irq_find_mapping查找virq

注册/注销函数:

  • 注册:gpiochip_add_data(&gc, 私有数据指针) —— 第二个参数通过gpiochip_get_data(gc)取回
  • 注销:gpiochip_remove(&gc)

中断部分——irq_chip

irq_chip描述子中断控制器的硬件操作方法,怎么屏蔽、使能、确认中断、设置触发类型

需要实现的回调:

回调 功能 操作寄存器
irq_mask 屏蔽某引脚中断 读-改-写IMR,清对应bit
irq_unmask 使能某引脚中断 读-改-写IMR,置对应bit
irq_ack 清除中断状态 往ISR对应bit写1(write-1-to-clear)
irq_set_type 设置触发类型 写ICR1/ICR2(2bit编码)+ EDGE_SEL(双边沿)+ 切换流控handler

irq_chip不需要注册,在irq_domain的map回调里通过irq_set_chip_and_handler关联到具体的virq。

中断部分——irq_domain及其ops

irq_domain管理hwirq(引脚号0~31)到virq(Linux虚拟中断号)的映射。使用linear方式创建。

irq_domain_ops需要实现的回调:

回调 功能 实现方式
map 建立映射时的初始化 调irq_set_chip_and_handler关联irq_chip + 调irq_set_chip_data设私有数据
xlate 翻译设备树中断描述 调用内核函数irq_domain_xlate_twocell(#interrupt-cells=2的标准格式)

创建方式: irq_domain_add_linear(node, 32, &ops, 私有数据)

注意创建domain后立即循环调irq_create_mapping(domain, i)为32个引脚全部建好映射,确保后续gpiod_to_irq能立即找到对应的virq,这是使用linear domain的GPIO驱动的常见做法,这样可以保证设备驱动使用gpiod_get_irq的时候map已经建立

中断部分——chained handler

链式中断处理函数,在GIC父中断触发时被调用,负责读ISR寄存器分辨是哪个引脚触发的。

注册方式: irq_set_chained_handler_and_data(父中断号, handler函数, 私有数据) —— GPIO5有2根父中断线,注册2次,共用同一个handler。


三、硬件基础知识

GPIO

GPIO(General Purpose Input/Output)是SoC上最简单的外设。输出模式下CPU写数据寄存器控制引脚电平(高/低),输入模式下CPU读状态寄存器获取外部信号电平。IMX6ULL有GPIO1~GPIO5共5组,每组32个引脚,寄存器布局一致,仅基地址不同。

GPIO中断与子中断控制器

GPIO中断有5种触发类型:低电平、高电平,上升沿、下降沿、双边沿。

GPIO中断区别于SPI/I2C/UART中断的核心点:多对一共享。以GPIO5为例,32个引脚共享2根GIC中断线(pin0-15一根,pin16-31一根)。GIC只知道"GPIO5有中断",但不知道是哪个引脚,因此需要子中断控制器读ISR寄存器分辨

完整数据流:外部信号变化 → ISR对应bit置1 → IMR对应bit为1(已使能)→ 父中断线通知GIC → GIC调chained handler → handler读ISR & IMR得到pending → 遍历每个pending bit调generic_handle_irq → 流控handler(handle_edge_irq/handle_level_irq)调irq_ack和设备驱动handler

边沿触发 vs 电平触发的流控差异

边沿触发(handle_edge_irq):先ack清状态再调handler——防止处理期间丢新边沿。

电平触发(handle_level_irq):先mask屏蔽,调handler,再unmask——因为电平一直在,不先mask会反复触发。

irq_set_type里需要根据触发类型调irq_set_handler_locked切换对应的流控handler。


四、NXP手册

硬件结构(取自NXP官方芯片手册1346页)

在这里插入图片描述

寄存器

8个寄存器,每个32bit,每bit对应一个引脚(bit N对应pin N)。

GPIO功能寄存器(3个):

寄存器 偏移 读/写 说明
DR 0x00 读写 数据寄存器,写设输出电平,读返回输出锁存值
GDIR 0x04 读写 方向寄存器,0=输入,1=输出
PSR 0x08 只读 引脚状态寄存器,反映引脚真实物理电平

中断功能寄存器(5个):

寄存器 偏移 读/写 说明
ICR1 0x0C 读写 中断配置,pin0-15,每引脚2bit
ICR2 0x10 读写 中断配置,pin16-31,每引脚2bit
IMR 0x14 读写 中断屏蔽,1=使能,0=屏蔽
ISR 0x18 读写 中断状态,1=挂起,write-1-to-clear
EDGE_SEL 0x1C 读写 双边沿选择,1=强制双边沿(覆盖ICR)

ICR 2bit编码:

编码 触发类型
00 低电平触发
01 高电平触发
10 上升沿触发
11 下降沿触发

ICR bit位置计算:pin N < 16时在ICR1的[2N+1:2N]位,pin N >= 16时在ICR2的[2(N-16)+1:2(N-16)]


五、同步互斥

内核框架提供的保护

GPIO框架(gpiolib): 有全局gpio_lock保护gpio_desc表,但调gpio_chip回调时不持任何锁。两个线程可以同时进入set函数操作同一个DR寄存器。

IRQ框架: 每个irq_desc有自己的desc->lock。handle_edge_irq/handle_level_irq在调irq_chip回调前持有该锁并关中断。但不同引脚是不同的irq_desc、不同的锁,两个不同引脚的mask操作可以并发操作同一个IMR寄存器。

跟SPI/I2C框架自动保护不同,GPIO和IRQ框架都不保护共享硬件寄存器的读-改-写操作,需要驱动自己加锁。

驱动中的spinlock保护

在驱动私有数据中定义一把spinlock,保护所有读-改-写操作。使用spin_lock_irqsave/spin_unlock_irqrestore,因为gpio回调可能在进程上下文被调用,irq_chip回调在中断上下文被调用,irqsave版本两种场景都安全


六、设备树配置

官方GPIO节点分析(imx6ull.dtsi)

gpio5: gpio@020ac000 {
    compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
    reg = <0x020ac000 0x4000>;
    interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,   /* pin0-15共享 */
                 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;    /* pin16-31共享 */
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
};

父中断是电平触发(LEVEL_HIGH),这意味着只要ISR中有pending且IMR使能,父中断线一直保持高。而且imx6ull.dtsi中GPIO节点没有clocks属性(GPIO时钟always-on)也没有gpio-ranges属性

自己的板级设备树修改

覆盖&gpio4和&gpio5的compatible为自己的,继承dtsi中的reg/interrupts/gpio-controller/interrupt-controller等属性。为GPIO5添加gpio-ranges = <&iomuxc_snvs 0 0 10>(10个SNVS引脚),为GPIO4添加gpio-ranges = <&iomuxc 0 94 17>, <&iomuxc 17 117 12>(29个物理引脚分两段映射,NAND域17个+CSI域12个,不是32因为IO29-IO31无物理pad)。
禁用reg_gpio_dvfs释放GPIO5_IO03给LED测试。
中断测试引脚选GPIO4_IO19(CSI_VSYNC pad,在扩展板有引出且CSI已disable),pinctrl配置100K上拉+迟滞(0x1B000)
在这里插入图片描述

GPIO-Pinctrl联动机制

gpio-ranges建立了GPIO引脚号到pinctrl引脚号的映射。当sysfs export或gpiod_get请求一个引脚时:

gpio_request → chip->request = gpiochip_generic_request
  → pinctrl_gpio_request → 查gpio-ranges找到pinctrl引脚编号
  → 调官方pinctrl的gpio_request_enable → 写IOMUXC寄存器复用为GPIO功能

这样不需要单独写pinctrl节点,引脚复用是自动完成的。


七、测试设备驱动

为验证中断功能编写了一个简单的测试设备驱动,结构如下:

probe:

  1. devm_gpiod_get获取GPIO引脚(触发gpio_chip->request → pinctrl复用)
  2. gpiod_to_irq获取virq(触发gpio_chip->to_irq → irq_find_mapping)
  3. devm_request_irq注册中断handler(触发irq_set_type写ICR + irq_unmask写IMR)

中断handler: 递增cout++, printk

这个测试驱动同时验证了gpio_chip的request/to_irq回调、irq_domain map、irq_set_type、irq_unmask、以及实际中断触发时的chained handler→irq_ack→handler完整链路


八、测试验证

驱动加载检查

insmod gpio_interrupt_adapter.ko
lsmod | grep gpio                          # 确认加载
cat /sys/kernel/debug/gpio                 # 确认两组GPIO控制器注册
cat /sys/class/gpio/gpiochipN/label        # 确认label为自己的名字

加载后debugfs中出现两个"zxr-my_gpio"控制器:GPIO5(base=448, 32pins)和GPIO4(base=480, 32pins)。

GPIO功能测试(LED)

测试结果:LED正确响应亮灭,验证direction_output和set函数操作DR/GDIR寄存器正确。

中断功能测试(SR501)

硬件接线:SR501的VCC→5V,GND→GND,OUT→GPIO4_IO19。

在这里插入图片描述

/proc/interrupts的每个字段确认:virq=194分配成功、中断控制器名"zxr-gpio-irq"正确、hwirq=19正确、Edge触发类型正确。

SR501触发后中断正确触发和处理,验证了irq_domain map、irq_set_type、irq_unmask、chained handler、irq_ack的完整链路。


九、调试分析

trace_event追踪中断事件

使用Ftrace的trace_event机制,启用irq_handler_entry/exit事件:
在这里插入图片描述
结果:

在这里插入图片描述

handler耗时3.256ms(主要是printk的console输出开销)。

Ftrace function_graph追踪完整调用链

使用set_graph_function锁定从chained handler入口开始追踪:

echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo my_gpio_irq_handler > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 触发中断...
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace

trace展示了完整的函数调用层级:
在这里插入图片描述

my_gpio_irq_handler()                      总耗时5660us
├── irq_find_mapping()               4us   查hwirq→virq映射
├── generic_handle_irq()           5577us   分发给流控handler
│   └── handle_edge_irq()
│       ├── my_irq_ack()             20us   写ISR清中断状态
│       └── handle_irq_event()
│           └── my_test_irq_handler()
│               └── printk()       ~5000us  console输出(耗时主因)
│                   └── imx_console_write()
│                       └── uart_console_write()
│                           └── imx_console_putchar() × 40+次
└── irq_chip_eoi_parent()            23us   通知GIC处理完毕
    └── gic_eoi_irq()

驱动本身的中断处理代码(ack+分发+eoi)总耗时约127us,不到总耗时的2.3%。5000us(88%)的开销全部来自printk的串口console逐字符输出——这印证了产品驱动中断处理函数里不能用printk,复杂操作应放到下半部。

十、遇到的问题与解决

问题:设备树里GPIO节点没有clocks属性导致probe失败

现象: probe在devm_clk_get处返回错误。

原因: IMX6ULL的GPIO时钟是always-on的,dtsi中GPIO节点不含clocks属性。

我的解决: 将clk获取改为可选——devm_clk_get失败时将clk指针置NULL而非返回错误,remove中检查非NULL才disable。

问题:裸杜邦线触发中断导致开发板重启

现象: 杜邦线碰GND瞬间开发板重启。

原因: 接触弹跳产生高频边沿。父中断为电平触发,ISR持续有pending导致chained handler无限循环,CPU被占满触发看门狗重启。

我的解决: 改用SR501模块,输出信号干净无弹跳。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐