拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

飞腾FT2000/4 GPIO与中断驱动开发实战:从设备树到按键消抖

飞腾FT2000/4 GPIO与中断驱动开发实战:从设备树到按键消抖 飞腾FT2000/4这几年出镜率很高4核ARMv8架构性能和功耗比较均衡工控、嵌入式、边缘计算设备里用得很广。但真上手做底层开发的时候关于GPIO和中断的实用资料特别少网上搜到的要么是x86那一套要么是单片机的经验照搬到飞腾平台上经常对不上号。这篇文章是我自己在FT2000/4上从GPIO基础配置到中断驱动开发的完整记录包括设备树怎么解析、GPIO编号怎么算、中断链路怎么走、按键消抖怎么做最后还整理了一份排查速查表。适合正在做飞腾FT2000/4或D2000平台外设驱动的工程师也适合刚接触ARM平台、被GPIO和中断绕晕的同学。1. 先摸清FT2000/4的GPIO硬件底子1.1 芯片与GPIO控制器概貌FT2000/4是一颗4核心ARMv8处理器片内外设相当丰富集成DDR、PCIe、USB、GMAC、UART、I2C、SPI等常用接口。芯片上的GPIO控制器由多个bank组成每个bank对应一组32位的引脚引脚数量按封装不同有所差异常见板卡上可用GPIO基本都在100个以上实际产品中经常用到的十几个引脚通常都集中在控制器的前几个bank。GPIO控制器的寄存器布局很常规核心就是数据寄存器、方向寄存器、中断控制寄存器这几类。方向寄存器写1表示输出、写0表示输入数据寄存器负责读写引脚电平。这里有个细节很多人会忽略FT2000/4的GPIO控制器在不同BSP版本里寄存器偏移定义可能略有不同飞腾的SDK和公开代码里有完整的头文件定义开发前最好先把phy_gpio.h之类的寄存器定义从头到尾看一遍不要等到调试时才发现方向寄存器的位定义和自己想的不一样。中断控制相关的寄存器至少包含中断状态寄存器、中断屏蔽寄存器、中断触发方式寄存器、中断清除寄存器。引脚电平发生跳变后由边沿检测电路捕获并置位状态位如果该引脚的中断没有被屏蔽就会向中断控制器发出请求。这个机制本身和大多数ARM SoC的GPIO没有本质区别飞腾的差异更多体现在中断号的映射方式上后面专门讲。1.2 与x86平台开发的思维差异很多从x86转到飞腾平台的朋友一开始最不适应的就是“操作GPIO的方式怎么全变了”。在x86平台上操作GPIO往往是直接读写IO端口或者通过ACPI去描述某个设备用了哪个GPIO。传统x86主板上很多GPIO是挂在Super I/O芯片里的BIOS帮你初始化好应用层基本不需要关心引脚复用和中断映射。到了飞腾这种ARM平台上这套经验基本作废。ARM平台外设开发的典型套路是设备树描述硬件连接关系pinctrl子系统管理引脚复用gpiolib提供统一的GPIO操作接口中断子系统负责把硬件中断映射成Linux IRQ号。你在x86上可能从来不需要打开设备树文件但在飞腾平台上设备树就是开发者与硬件之间的“契约”不读懂它后面寸步难行。这种差异本质上是平台设计哲学的差异。x86讲究“BIOS帮你搞定一切”ARM平台讲究“软件自己掌控一切”。所以刚转过来的同学我建议第一周别急着写代码先把设备树、pinctrl、gpiolib这三个概念吃透后面开发会顺畅很多。1.3 GPIO工作模式与复用关系说到GPIO的工作模式网上经常有人提“GPIO的8种工作模式”这个概念更多是针对单片机比如STM32的输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、复用开漏、复用推挽这8种。到了飞腾这类Linux SoC上概念不完全一样但思想相通。GPIO引脚在SoC内部往往连接着多个外设控制器比如某个引脚既可以被UART控制器使用也可以被PWM控制器使用还可以做普通GPIO。具体当什么用由pinctrl子系统决定。pinctrl控制的就是引脚的“身份切换”。设备树里一个节点声明了pinctrl-names default和pinctrl-0 pinctrl_uart1那内核在驱动probe的时候就会自动把相关引脚切换成UART功能。这时候你要是还想去操作这个引脚做GPIO输入输出就会失败或者产生干扰。从我实际经验来看飞腾平台GPIO开发最常见的翻车原因不是不会操作GPIO而是没有搞清楚某个引脚当前被谁占用。查复用状态有个笨但有效的办法打开板级设备树搜索你想用的引脚编号看它在哪些节点里出现过。如果一个引脚被UART节点声明了又被GPIO使用者节点声明了那说明硬件设计本身就有问题或者设备树存在冲突需要找硬件同事确认。2. 环境准备内核配置、设备树与GPIO编号计算2.1 确认内核GPIO子系统配置正式开始写代码之前先确认系统里的内核是否已经打开了GPIO相关配置。不管用的是银河麒麟V10 ARM版还是自己编译的发行版都要检查这几个关键配置项。zcat /proc/config.gz | grep GPIO重点关注CONFIG_GPIOLIBGPIO子系统的核心开关必须打开CONFIG_GPIO_CDEV提供/dev/gpiochip*字符设备和新的用户态API新项目依赖这个CONFIG_GPIO_SYSFS老式的sysfs接口新内核默认关闭一般不建议再依赖如果手头的系统跑的是自编译内核配置阶段最好把CONFIG_GPIOLIB和CONFIG_GPIO_CDEV都选中然后把对应平台的GPIO驱动编进内核而不是模块。编成模块也可以但要注意initramfs里有没有包含这个模块否则内核起来以后模块没加载GPIO节点根本不会出现。2.2 从设备树里读GPIO控制器信息飞腾平台在启动阶段会通过设备树把硬件信息告诉内核。要查看当前系统的设备树有两条路一是直接看运行时设备树二是用dtc反编译。# 查看设备树根节点下的子节点 ls /proc/device-tree/ # 反编译整个设备树 dtc -I fs -O dts /proc/device-tree -o dumped.dts反编译出来的dts文件可以直接搜索GPIO控制器节点。飞腾FT2000/4的GPIO控制器节点大致长这样不同BSP版本可能有差异以实际设备树为准gpio0: gpio2800000 { compatible phytium,gpio; reg 0x0 0x2800000 0x0 0x1000; interrupts GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH; gpio-controller; #gpio-cells 2; interrupt-controller; #interrupt-cells 2; };几个关键属性分别说明一下。reg定义的是GPIO控制器的寄存器基地址和地址范围interrupts定义的是整个GPIO控制器占用GIC的哪个SPI中断号gpio-controller表明这个节点是一个GPIO控制器#gpio-cells 2表示引用这个控制器里的引脚时需要提供2个32位整数第一个是引脚号第二个是GPIO标志比如GPIO_ACTIVE_HIGH、GPIO_ACTIVE_LOWinterrupt-controller和#interrupt-cells则表示这个GPIO控制器同时也是引脚级的中断控制器。interrupts属性非常关键它决定了所有GPIO引脚的中断最终通过哪条线到达CPU。FT2000/4的GPIO控制器一共占用几个SPI中断要看具体硬件版本有的版本几个bank共用一条SPI有的版本每个bank一条。这个信息在内核启动日志里也能看到dmesg | grep gpio会打印GPIO控制器注册信息。2.3 GPIO全局编号的计算方法设备树里描述的是“某个控制器的第几号引脚”但Linux内核最终会把每个控制器下的引脚统一编号成全局GPIO号。这个全局号由gpiolib动态分配分配顺序和注册顺序相关。举个实际例子。假设系统里有gpiochip0和gpiochip1两个控制器gpiochip0有32个引脚gpiochip1有16个引脚。那么gpiolib可能把gpiochip0的引脚映射到全局号0到31把gpiochip1的引脚映射到32到47。听起来很简单但问题在于如果控制器的注册顺序变了或者其中一个控制器初始化失败全局号就会整体偏移。所以在应用层写死全局GPIO号是非常危险的做法这也是为什么我强烈推荐新项目直接使用libgpiod。libgpiod按chip line的方式定位引脚不依赖全局编号就算系统里有多个GPIO控制器只要芯片名固定操作就是稳定的。在内核驱动里则不用纠结这么多设备树里用gpios gpio0 5 GPIO_ACTIVE_HIGH这种方式引用驱动里通过devm_gpiod_get()拿到GPIO描述符一切都由框架帮你映射好。3. GPIO输入输出实操从点灯到读取按键电平3.1 用libgpiod快速验证引脚通道环境准备做好以后先用命令行工具快速验证一下引脚能不能正常操作这一步能省掉后面写代码时的很多疑问。# 安装gpiod工具Debian/Ubuntu系 sudo apt install gpiod # 查看系统中有哪些GPIO控制器 gpiodetect # 查看某个控制器的详细引脚信息 gpioinfo gpiochip0gpioinfo的输出信息量很大每一行对应一个引脚会显示引脚当前是否被占用、被谁占用、方向是什么、活动电平配置等。调试前先看一眼这个基本能定位一半的问题。验证输出功能# 把gpiochip0的第5号引脚拉高 gpioset gpiochip0 51 # 拉低 gpioset gpiochip0 50验证输入功能# 读gpiochip0的第6号引脚电平 gpioget gpiochip0 6还有一个小技巧用gpioinfo监控引脚状态变化相当于一个简易逻辑分析仪。虽然在gpioinfo的默认视图里看不到实时变化但配合gpioget轮询也能凑合调试。如果要做严格的时序分析还是得示波器或者逻辑分析仪上。3.2 用C代码操作GPIO输出点亮LED命令行工具只是验证工具产品代码还是要用API。以libgpiod v1.x为例贴一段最简单的输出控制代码。#include stdio.h #include gpiod.h int main(void) { struct gpiod_chip *chip; struct gpiod_line *line; int ret; chip gpiod_chip_open_by_name(gpiochip0); if (!chip) { perror(open chip failed); return -1; } line gpiod_chip_get_line(chip, 5); if (!line) { perror(get line failed); gpiod_chip_close(chip); return -1; } /* 请求输出设置初始电平为1 */ ret gpiod_line_request_output(line, led_ctl, 1); if (ret 0) { perror(request output failed); gpiod_chip_close(chip); return -1; } /* 拉低 */ gpiod_line_set_value(line, 0); sleep(1); /* 拉高 */ gpiod_line_set_value(line, 1); gpiod_line_release(line); gpiod_chip_close(chip); return 0; }编译命令gcc -o gpio_led gpio_led.c -lgpiod这里有个细节值得注意gpiod_chip_open_by_name里的“gpiochip0”来自gpiodetect输出不同板卡上名字可能不一样。有些板卡存在多个GPIO控制器靠名字打开最可靠但名字本身就是设备树里节点的别名和芯片物理位置不一定完全对应需要认真核对。GPIO_ACTIVE_LOW是另一个容易踩的坑。如果设备树里把某个LED节点声明成了GPIO_ACTIVE_LOW那gpiod_set_value(line, 1)实际上是输出低电平点亮LED。这并不奇怪而是内核帮你做了逻辑反转——你在软件层操作的是“逻辑值”硬件电平由标志位自动转换。理解了这一点就不会被“设置成1但LED灭了”这种现象搞懵。3.3 输入读取与上下拉配置读取输入比输出略微复杂一点因为涉及电平可靠性和外部电路。直接读按键这类简单场景需要注意两个问题引脚有没有被配置成输入外部电平在悬空时是不是一个确定状态。FT2000/4的GPIO输入通道在方向寄存器配置为输入之后数据寄存器读到的就是引脚当前电平。但如果引脚悬空电平会处于不确定状态可能读出来一会儿0一会儿1这就是为什么需要上下拉电阻。上下拉有两个来源芯片内部上下拉和外部分立电阻。飞腾的GPIO是否支持内部上下拉、上下拉电阻多大要查具体芯片手册。如果手册里没有明确说明最简单的方案就是在硬件上预留焊盘或者直接焊一个10kΩ电阻到电源或者地。在驱动层面配上下拉的方式有两种。如果设备树里GPIO节点配置了gpio-keys或者pinctrl已经把引脚默认状态定义好了那不需要在代码里处理。但如果是自己用gpiod API操作libgpiod v1.x本身不直接提供设置上下拉的接口需要找到对应的pinctrl实现或者在设备树里给引脚所在的bank节点增加pull-up配置。我自己常用的做法是按键电路优先采用低有效设计按键一端接GPIO另一端接地外部加上拉电阻。这样GPIO空闲时读到的是高电平按键按下时读到低电平逻辑清晰干扰也少。高有效的设计也不是不行但低有效配合上拉是工业设备里最常用的接法排查问题的时候也符合大部分工程师的习惯。4. 中断配置GPIO中断的完整链路与驱动实现4.1 GPIO中断从硬件到软件怎么走中断是GPIO开发里最容易让人懵的部分因为中间隔了好几层映射。我把这条链路完整拆开来讲。硬件层面某个GPIO引脚的电平发生跳变后GPIO控制器内部的边沿检测电路捕获到跳变把对应的中断状态寄存器位置1。如果这个引脚的中断没有在屏蔽寄存器里被屏蔽GPIO控制器就会通过中断请求线向GIC通用中断控制器发送一个中断信号。FT2000/4上用的GIC支持SPI共享外设中断和PPI私有外设中断GPIO控制器通常占用若干个SPI。Linux内核里GPIO控制器驱动在初始化时会把自己注册成一个IRQ Domain。所谓IRQ Domain就是负责把“硬件中断号”翻译成“Linux IRQ号”的中间层。GPIO控制器里的每个引脚可以被映射成一个独立的Linux IRQ号但这个IRQ号与GIC的SPI中断号是两个概念。实际的中断流程是GPIO引脚产生中断 → GPIO控制器通过SPI中断通知GIC → GIC向CPU发出IRQ → CPU查询GPIO控制器的中断状态寄存器 → 框架回调对应的GPIO驱动 → 最终调用到你在驱动里注册的中断处理函数。所以设备树里GPIO控制器节点的interrupts GIC_SPI 23 ...只是说“这个GPIO控制器整体通过GIC的第23号SPI上报告警”而你要在驱动里操作的IRQ号是通过gpiod_to_irq()转换出来的两者根本不是一个东西。把这个关系搞清楚了设备树里看到GPIO控制器占用SPI中断号就不会再困惑了。4.2 在设备树和驱动里申请GPIO中断现在写一个最基础的中断申请流程。假设板子上有一个按键接在gpio0的第6号引脚上按下为低电平。设备树里可以用gpio-keys这个标准框架来描述按键设备但在驱动开发的学习场景下自己写一个字符驱动会更清楚。先看设备树怎么描述GPIO中断资源。有两种风格一种是通过interrupts-extended直接引用GPIO控制器下的某个引脚作为中断源my_device: my-device { compatible my,gpio-irq-device; interrupts-extended gpio0 6 IRQ_TYPE_EDGE_FALLING; };另一种是在probe函数里动态获取GPIO描述符再调用gpiod_to_irq()把GPIO转成IRQ。这种方式更适合做通用驱动推荐使用。核心代码如下#include linux/interrupt.h #include linux/of_gpio.h #include linux/gpio/consumer.h struct my_priv { struct gpio_desc *gpio; int irq; }; static irqreturn_t my_irq_handler(int irq, void *data) { struct my_priv *priv data; /* 中断处理 */ pr_info(GPIO interrupt triggered\n); return IRQ_HANDLED; } static int my_probe(struct platform_device *pdev) { struct my_priv *priv; int ret; priv devm_kzalloc(pdev-dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; /* 获取GPIO描述符默认请求输入 */ priv-gpio devm_gpiod_get(pdev-dev, my, GPIOD_IN); if (IS_ERR(priv-gpio)) { dev_err(pdev-dev, failed to get gpio\n); return PTR_ERR(priv-gpio); } /* 把GPIO转成IRQ号 */ priv-irq gpiod_to_irq(priv-gpio); if (priv-irq 0) { dev_err(pdev-dev, failed to map gpio to irq\n); return priv-irq; } /* 申请中断 */ ret devm_request_irq(pdev-dev, priv-irq, my_irq_handler, IRQF_TRIGGER_FALLING, my_gpio_irq, priv); if (ret) { dev_err(pdev-dev, failed to request irq\n); return ret; } return 0; }设备树里对应的节点要给出GPIO引用my_device: my-device { compatible my,gpio-irq-device; my-gpios gpio0 6 GPIO_ACTIVE_LOW; };注意devm_gpiod_get获取GPIO时默认就会请求输入方向不需要额外调用gpiod_direction_input。这一点和老的gpio_requestgpio_direction_inputAPI不同新的gpiod API更安全省事。还有一个容易忽略的地方devm_gpiod_get会检查设备树引脚的GPIO_ACTIVE_LOW标志。如果你在设备树里写了GPIO_ACTIVE_LOW那gpiod_get_value读回来的值会被反转。中断的触发方式IRQF_TRIGGER_FALLING则是基于物理电平的不会随着GPIO_ACTIVE_LOW反转。这两套逻辑容易把人绕晕我的建议是设备树里GPIO标志统一用GPIO_ACTIVE_HIGH电平逻辑自己在代码里处理不要依赖框架的反转省得排查问题的时候还要算一遍。4.3 线程化中断与中断处理函数注意事项很多对中断的理解还停留在“ISR里不能做耗时操作”这个阶段这句话没错但在现代Linux下需要进一步细分。传统的中断上下文里不能调用可能导致睡眠的函数比如msleep、wait_event、mutex_lock、kmalloc(..., GFP_KERNEL)都不行因为中断上下文不是一个可调度的进程上下文。如果你想在中断处理里做这些事就不能用普通的request_irq而应该用request_threaded_irq或者devm_request_threaded_irq申请线程化中断。线程化中断的基本思路是把耗时操作放到一个内核线程里执行。主处理函数只做最必要的事情然后返回IRQ_WAKE_THREAD内核会唤醒一个线程去执行你提供的线程处理函数。上面的代码可以改一下static irqreturn_t my_irq_handler(int irq, void *data) { /* 快速处理只做标记 */ return IRQ_WAKE_THREAD; } static irqreturn_t my_irq_thread(int irq, void *data) { struct my_priv *priv data; /* 这里可以睡眠可以做耗时操作 */ msleep(20); pr_info(GPIO irq thread executed\n); return IRQ_HANDLED; } /* 申请线程化中断 */ ret devm_request_threaded_irq(pdev-dev, priv-irq, my_irq_handler, my_irq_thread, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, my_gpio_irq, priv);注意IRQF_ONESHOT这个标志。它在硬件中断处理完成后、线程化处理函数执行完之前会保持中断线处于屏蔽状态避免在中断处理期间再次触发。对于电平触发或者共享中断的场景没有这个标志容易导致中断风暴。我自己的经验是只要用线程化中断就默认加上IRQF_ONESHOT除非有非常明确的理由不加。4.4 用户态监听GPIO事件的方法有些应用场景不需要写内核驱动直接在用户态监听GPIO事件就够了比如一个简单的门禁记录程序、传感器报警程序。libgpiod提供了非常方便的事件监听接口。#include stdio.h #include gpiod.h int main(void) { struct gpiod_chip *chip; struct gpiod_line *line; struct gpiod_line_event event; int ret; chip gpiod_chip_open_by_name(gpiochip0); if (!chip) { perror(open chip failed); return -1; } line gpiod_chip_get_line(chip, 6); if (!line) { perror(get line failed); gpiod_chip_close(chip); return -1; } /* 请求双边沿事件监听 */ ret gpiod_line_request_both_edges_events(line, event_monitor); if (ret 0) { perror(request event failed); gpiod_chip_close(chip); return -1; } for (;;) { /* 阻塞等待事件第二个参数传NULL表示无限等待 */ ret gpiod_line_event_wait(line, NULL); if (ret 0) { gpiod_line_event_read(line, event); printf(event type: %d, timestamp: %llu\n, event.event_type, (unsigned long long)event.ts.tv_sec * 1000000000ULL event.ts.tv_nsec); } } gpiod_line_release(line); gpiod_chip_close(chip); return 0; }gpiod_line_event_wait底层基于poll/epoll实现返回之后事件已经在内核里排队了调用gpiod_line_event_read不会阻塞。如果多个事件在短时间内到达需要循环读取直到read返回-1。这个API很适合做应用层的边沿检测省去了自己读电平、判断边沿的麻烦。不过要提醒的是用户态事件监听的延迟比内核线程化中断要大一些。因为事件要从内核通过字符设备传到用户态中间有上下文切换和文件描述符唤醒的开销。对实时性要求高的场景还是得走内核中断。5. 实战案例一个带消抖的按键中断模块5.1 需求与硬件连接方案把前面讲的东西串起来做一个完整的实战模块。需求描述设备上有两个物理按键一个“功能键”一个“复位键”按下瞬间需要触发系统执行特定动作。硬件上一共两个引脚一个接GPIO A用于功能键一个接GPIO B用于复位键。按键按下时引脚电平从高变低低有效按键抬起时从低变高。硬件连接很简单按键一端接GPIO引脚另一端接地GPIO引脚外部接一个10kΩ上拉电阻到3.3V在按键两端再并联一个0.1μF陶瓷电容用于硬件层面的抖动滤除这个电路在飞腾评估板上做验证很典型。10kΩ上拉电阻确保空闲状态电平稳定在3.3V0.1μF电容配合按键接触电阻形成一个简单的RC低通滤波器能干掉大部分机械抖动的高频分量。但RC滤波并不能完全消除抖动所以软件消抖仍然要做两个手段叠加才可靠。5.2 驱动代码实现下面是一个完整的按键中断驱动框架用到线程化中断加延时确认消抖。这个方案比传统的“定时器轮询”更符合中断驱动的设计思路先用硬件中断捕获事件再通过线程化处理函数确认电平状态最后执行业务动作。#include linux/module.h #include linux/platform_device.h #include linux/interrupt.h #include linux/gpio/consumer.h #include linux/delay.h #include linux/jiffies.h struct key_priv { struct gpio_desc *gpio; int irq; int key_id; }; static void key_action(int key_id) { /* 实际业务动作比如上报input事件、执行某个操作 */ pr_info(key %d action executed\n, key_id); } static irqreturn_t key_irq_thread(int irq, void *data) { struct key_priv *priv data; int val; /* 先延迟一小段时间等电平稳定 */ msleep(20); /* 再读一次电平确认按键状态 */ val gpiod_get_value(priv-gpio); if (val 0) { /* 确认按下执行按键动作 */ key_action(priv-key_id); } return IRQ_HANDLED; } static int key_probe(struct platform_device *pdev) { struct key_priv *priv; struct device *dev pdev-dev; int ret; priv devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; platform_set_drvdata(pdev, priv); dev-platform_data NULL; /* 从设备树获取GPIO */ priv-gpio devm_gpiod_get(dev, key, GPIOD_IN); if (IS_ERR(priv-gpio)) { dev_err(dev, failed to get key gpio\n); return PTR_ERR(priv-gpio); } /* 从设备树获取key-id属性 */ ret device_property_read_u32(dev, key-id, priv-key_id); if (ret) priv-key_id 0; priv-irq gpiod_to_irq(priv-gpio); if (priv-irq 0) { dev_err(dev, failed to map gpio to irq: %d\n, priv-irq); return priv-irq; } /* 申请线程化中断下降沿触发IRQF_ONESHOT保证处理期间屏蔽 */ ret devm_request_threaded_irq(dev, priv-irq, NULL, key_irq_thread, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, key_irq, priv); if (ret) { dev_err(dev, failed to request irq: %d\n, ret); return ret; } dev_info(dev, key driver probed, irq%d\n, priv-irq); return 0; } static const struct of_device_id key_of_match[] { { .compatible example,key-irq }, { } }; MODULE_DEVICE_TABLE(of, key_of_match); static struct platform_driver key_driver { .probe key_probe, .driver { .name key_irq, .of_match_table key_of_match, }, }; module_platform_driver(key_driver); MODULE_LICENSE(GPL);设备树里对应节点key1: key-irq0 { compatible example,key-irq; key-gpios gpio0 10 GPIO_ACTIVE_LOW; key-id 1; }; key2: key-irq1 { compatible example,key-irq; key-gpios gpio0 11 GPIO_ACTIVE_LOW; key-id 2; };这段代码的消抖逻辑很直观中断触发后线程处理函数先睡20ms再判断引脚电平。如果确实为低说明按键真的按下了如果已经变回高说明是抖动或者干扰忽略掉这次事件。这个方案有个小缺陷在处理函数睡眠的20ms期间如果同一个按键又发生了抖动由于IRQF_ONESHOT的存在新中断不会被立即处理但事件会被挂起或丢失。对于普通按键场景这个损失可以接受。5.3 消抖原理与参数选取消抖这件事看着简单实际有很多细节。机械按键的抖动时间通常在5ms到20ms之间品质差的按键可能更长。RC电路的时间常数、软件延时时间、业务对响应速度的要求这三个参数需要综合权衡。我给的20ms延时是基于大部分普通按键的实测经验。如果用的是高品质轻触开关5ms到10ms就够如果是品质一般的机械开关建议保守一点30ms更稳妥。延时太长会影响按键响应手感延时太短又可能没滤干净抖动。硬件RC参数方面0.1μF电容加10kΩ上拉电阻的时间常数是1ms左右τ RC 10kΩ × 0.1μF 1ms这个组合能滤掉高频噪声但挡不住完整的机械抖动串。所以硬件滤波不能完全替代软件消抖两者配合才可靠。还有一个进阶技巧在驱动里记录上一次稳定的按键状态只在状态发生翻转时上报事件。这种方法比单纯的延时确认更健壮因为它天然过滤了重复触发。代码层面也不复杂用atomic_t或者spinlock保护一个状态变量就行。5.4 验证与调试方法驱动写好以后先在开发板上验证。几个常用的验证手段# 查看驱动是否成功注册中断是否申请成功 cat /proc/interrupts | grep key # 查看内核日志里的probe信息 dmesg | tail -20 # 查看GPIO状态 gpioinfo gpiochip0 | grep -E line 10|line 11/proc/interrupts是调试中断最直观的入口。每一列对应一个CPU核心数值表示该中断在这个核心上触发了多少次。按键每按一次对应的中断计数应该增加。如果计数不动说明中断根本没触发如果一次按键计数增加了好几十次说明消抖没做干净或者触发方式配置不对。用逻辑分析仪抓GPIO波形是更彻底的验证方法。把探头夹在按键引脚上按下按键观察波形按下瞬间应该有一串毛刺然后电平被拉低松开时同样有一串毛刺然后电平恢复高。理想情况下软件消抖后只产生一次事件这个用/proc/interrupts的计数变化就能间接验证。如果板子上实在没有逻辑分析仪也可以用gpioget写一个简单的shell轮询脚本把电平变化过程记录下来凑合分析抖动情况。但这个方法精度太低只能看个大概真正做产品验证还是建议上仪器。6. 高频问题排查与避坑指南6.1 常见问题速查表开发中最常遇到的情况整理成速查表按症状排查。症状可能原因排查方法GPIO输出电平不变化方向没配成输出引脚被pinctrl占用了其他功能用gpioinfo看方向查设备树该引脚是否被别的节点引用读取输入电平一直为0或1引脚悬空外部上下拉没接好引脚被配置成输出检查外部电路用万用表量引脚电压确认方向寄存器中断完全不触发设备树中断映射错误IRQ号没拿到触发方式不对cat /proc/interrupts看是否注册检查gpiod_to_irq返回值中断触发一次但多次响应机械抖动没滤干净触发方式配成了电平触发增加消抖延时确认是边沿触发检查IRQF_TRIGGER配置中断频繁触发刷屏引脚悬空电平漂移中断共享处理不当外部干扰加上下拉电阻示波器抓波形检查硬件电路操作GPIO报Invalid argumentGPIO号不在有效范围引脚正被其他驱动占用用gpioinfo确认引脚号lsof查看占用进程用户态这张表基本覆盖了我在飞腾平台上调试GPIO遇到过的绝大多数问题。有一个经验可以分享遇到GPIO问题先别急着改代码先用gpioinfo、cat /proc/interrupts、dmesg这三个工具看一下当前状态很多时候问题一下就暴露了比瞎猜代码快得多。6.2 引脚复用冲突与扩展场景FT2000/4的引脚复用冲突是实实在在的坑尤其是评估板和实际产品板之间经常有差异。比如某个引脚在评估板上被设计成普通GPIO但到了定制板卡上被复用成了UART流量控制信号直接操作它就会出问题。排查复用冲突的方法前面提过反编译设备树搜索目标引脚号看它出现在哪些节点。但设备树里搜不到不代表绝对安全因为有些引脚在Bootloader阶段就被配置成了别的功能Linux启动后根本没有对应的设备树节点。这种情况需要用飞腾的引脚复用配置工具或者直接读寄存器确认。另外实际项目中经常用“1路UART转16路GPIO扩展芯片”这种方案来扩充GPIO数量。这类芯片一般挂在UART或者I2C总线上通过协议转换芯片比如MCP23017、PCA9555等提供额外16个GPIO。接入飞腾平台后这些扩展GPIO在Linux里会注册成独立的gpiochip你可以像操作原生GPIO一样操作它们但要注意扩展GPIO不支持硬件中断的场景很常见很多I2C GPIO扩展芯片的中断功能需要额外的INT引脚连接到SoC的某个GPIO上才能用。6.3 中断异常的系统级排查思路中断触发异常是很棘手的因为可能的原因横跨硬件、设备树、驱动三层。我总结了一套排查步骤按顺序来效率最高。第一步确认中断有没有注册成功。看/proc/interrupts你的IRQ号应该在列表里并且对应的驱动名称正确。中断计数可能会增加——先确认这一点。第二步确认物理信号。用示波器或逻辑分析仪抓GPIO引脚波形对比按键动作是否产生了期望的边沿。如果波形本身就不对比如毛刺过多、电平不稳那是硬件问题软件再怎么调都没用。第三步确认触发方式。分两种情况边沿触发能不能匹配到实际跳变的极性电平触发有没有设置成合适的有效电平。如果GPIO引脚是高有效但你配置了IRQF_TRIGGER_FALLING那中断永远不会触发。第四步确认中断链路映射。在/sys/kernel/debug/irq/目录下可以查看每个IRQ的详细信息包括触发方式、状态、相关的设备名。如果中断链路中间某层映射错了这里能看出端倪。# 查看IRQ调试信息需要debugfs挂载 mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/irq/irqs第五步终极手段——直接读GPIO控制器寄存器。用devmem把GPIO控制器的中断状态寄存器、屏蔽寄存器全部读出来手动确认硬件层面的状态。这个方法需要对照芯片手册但往往能直接定位问题# 读取GPIO控制器某个偏移处的寄存器地址根据实际BSP修改 devmem 0x2800000中断风暴是另一个高发问题。表现是某根中断线的处理函数被反复调用CPU占用率飙升。常见原因有三个GPIO悬空导致电平在阈值附近抖动、中断触发方式配错比如电平触发但没加IRQF_ONESHOT、共享中断的处理函数没有正确判断中断源。排查时先把引脚外部电路处理好确认电平稳定再检查触发方式和中断标志一般都能解决。6.4 从FT2000/4迁移到D2000的注意事项顺便提一下飞腾D2000。D2000在架构上和FT2000/4一脉相承GPIO控制器的寄存器定义和中断机制基本一致本文的大部分内容可以直接移植。但有两个差异要注意一是D2000的CPU核心更多中断在多个核心之间的亲和性设置更值得优化二是D2000的板级设备树里GPIO控制器的基地址可能和FT2000/4不完全一样迁移时必须以新平台的设备树为准。我自己做迁移的经验是先对比两个平台设备树的GPIO节点重点看reg和interrupts属性确认硬件资源一致后再复用驱动代码。代码层面如果用的是gpiolib标准接口基本不用改如果直接操作寄存器那一定要重新核对寄存器偏移。中断亲和性方面D2000有更多CPU核心可以把不同按键的中断绑定到不同核心上降低单个核心的中断负载。操作方法是修改/proc/irq/irq/smp_affinity或者在内核里用irq_set_affinity_hint。这个优化在中断频率很高的时候效果明显普通按键场景意义不大但值得知道有这回事。个人在飞腾平台折腾GPIO这两年最大的感受就是资料少不等于做不了关键是先把硬件链路和Linux的软件框架对应起来。GPIO从输入到中断这一路硬件上不过是寄存器里的几个bit但软件上要摸清设备树、pinctrl、gpiolib、IRQ Domain、中断子系统这五座桥。每一座桥都不难走难的是把它们串起来看整体。只要把这条链路理顺了无论是FT2000/4、D2000还是其他ARM SoC开发思路都是相通的。实际调试中我还有一个习惯每动一个配置就同时看gpioinfo和/proc/interrupts的输出确认改动生效了再继续下一步这个习惯帮我避免了好多次“改了一堆东西但不知道哪里生效”的窘境。
返回列表