个人项目记录(七)GPIO和中断子系统控制器驱动(linear+链式):基于i.MX6ULL的嵌入式Linux终端系统构建与多子系统控制器驱动开发—GPIO和中断控制器驱动开发
本文是个人项目记录(七)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:
- devm_gpiod_get获取GPIO引脚(触发gpio_chip->request → pinctrl复用)
- gpiod_to_irq获取virq(触发gpio_chip->to_irq → irq_find_mapping)
- 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模块,输出信号干净无弹跳。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)