接触飞腾FT2000/4这板子之前,我一直在x86和普通ARM SoC上做Linux驱动,以为GPIO这种基础外设,拿到板子先看原理图,再翻芯片手册,配置一下寄存器、申请一下中断号,半小时就能跑通。真把银河麒麟V10(ARM64)烧进去、开始调FT2000/4的GPIO时才发现,这套东西的坑比想象中多:引脚复用、GPIO bank的编号规则、设备树里的pinctrl配置、用户态libgpiod与内核态驱动的选择,随便一个环节没理清,中断就是不来,电平就是读不对。这篇文章就把我从零开始在这块国产处理器上做GPIO输入读取、输出控制、边沿中断的完整过程写出来,适合正在飞腾FT2000/4、飞腾D2000等平台上做嵌入式Linux开发的朋友参考。
1. 飞腾FT2000/4的GPIO控制器,先别急着写代码
1.1 它不是树莓派那种“直接怼地址”的玩法
很多人第一次拿到飞腾FT2000/4,第一反应是像玩树莓派那样,找个物理地址,mmap一下,然后对寄存器一顿操作。FT2000/4确实是标准的ARM架构,这么做理论上可行,但实际工程里强烈不建议。原因很简单:飞腾平台的GPIO控制器不是简单几个寄存器就能打发的,它涉及引脚复用(Pin Mux)、上下拉配置、中断控制器对接等多个层面,如果绕过内核的GPIO子系统直接操作物理地址,等于把所有适配工作都揽到自己身上,而且一旦引脚被其他驱动占用,两个驱动打架的问题能把人折磨疯。
FT2000/4的GPIO在内部是分成多个bank管理的,每个bank有若干路引脚,整体挂在某个APB总线上,而中断则汇聚到GIC(通用中断控制器)。应用开发者看到的“GPIO编号”往往是芯片级别的物理引脚序号,但内核里操作GPIO口使用的是“控制器分组+组内偏移”的方式,这两者之间不是直观对应关系。换句话说,想靠图形界面看一眼就明白哪个编号对应哪个物理引脚,不太现实,必须回到原理图和设备树里去找。
1.2 先确认你手中板子的设备树状态
拿到一块FT2000/4板子,第一步不是写代码,而是先看系统里到底有哪些GPIO控制器。在终端下敲一行命令:
ls /sys/bus/platform/devices/ | grep gpio cat /proc/device-tree/soc/gpio*/compatible正常飞腾平台上会出现多个gpio节点,每个节点对应一个bank。有些板子的GPIO控制器兼容字段是类似“arm,pl061”的通用PL061控制器,有些则是飞腾自定义的控制器,具体以板卡厂商提供的设备树为准。这时候再配合原理图,找到目标引脚对应的ball名,然后在设备树里搜索ball名或者相关gpio节点的偏移,才能把物理引脚和软件编号对应上。
我实操时踩过一次很尴尬的坑:照着某篇网文用/dev/gpiochip0直接操作,发现读到的电平和万用表量出来的完全对不上。后来才发现,板卡厂商在设备树里把bank的顺序改过,/dev/gpiochip0并不是我默认以为的“第一组GPIO”。所以,动手前先跑一下:
gpiodetect看看实际有几个chip,每个chip的name是什么,gpioinfo再列出偏移对应的引脚名称,这一步能省掉后面90%的“为什么读数不对”的困惑。
2. 设备树里的GPIO配置,绕不开但没你想的那么神秘
2.1 你需要的其实是“复用”和“引用”两件事
飞腾平台上的GPIO引脚大多数不是独享的,同一物理引脚可能同时接UART、I2C、PWM或者其他功能,到底跑哪个功能,由设备树里的pinctrl决定。这个机制和STM32的GPIO_AF(复用功能)是同一个道理,只是Linux设备树的表达方式更啰嗦而已。
在飞腾平台上,普通GPIO输入输出其实不需要自己写节点,只要确认这个引脚没有被复用成其他功能就行。比如我要接一个按键到某个引脚,首先在设备树里确认这个引脚没有出现在UART或者其他外设的pinctrl-0属性里,然后就可以通过gpio-keys这类通用节点来声明,内核会自动把对应的引脚申请为输入并注册成input设备,连驱动都不用写。
一个典型的gpio-keys设备树节点大概长这样:
&gpio2 { keyboard_pins: keyboard_pins { pinctrl-single,pins = < 0x10 0x07 >; }; }; / { gpio-keys { compatible = "gpio-keys"; pinctrl-names = "default"; pinctrl-0 = <&keyboard_pins>; status = "okay"; key-power { label = "KEY_POWER"; linux,code = <116>; gpios = <&gpio2 3 GPIO_ACTIVE_LOW>; debounce-interval = <20>; }; }; };如果是自己写驱动,通常会在设备树里给自定义节点引用gpio:
&gpio0 { mydevice_pins: mydevice_pins { pinctrl-single,pins = < 0x14 0x07 >; }; }; mydevice { compatible = "vendor,mydevice"; pinctrl-names = "default"; pinctrl-0 = <&mydevice_pins>; input-gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>; output-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>; status = "okay"; };2.2 引脚编号到底怎么算
设备树里<&gpio0 5>这个“5”是什么意思,是很多新手最容易晕的地方。这里的5是相对于该gpio控制器基地址的偏移量,不是全局编号。比如全局GPIO号可能是128,但在这个bank内它就是编号5。在驱动里用gpiod_get获取struct gpio_desc后操作,内核自动处理了全局编号和chip偏移的换算,开发者无需关心。但是,在用gpiochip_get_base这类老接口,或者看/sys/kernel/debug/gpio里的调试信息时,会同时看到chip名字和hw偏移,需要搞清楚两者关系。
建议在实际开发中,建立一张自己的映射表,列清楚:物理ball名、bank名、组内偏移、设备树路径、用途,打印出来贴在工位旁边。这招在多人协作的项目里特别有用,因为飞腾的引脚编号资料往往分散在几千页的TRM里,每次翻pdf效率太低了。
3. 用户态快速验证:libgpiod是你最便宜的调试工具
3.1 银河麒麟V10下离线安装libgpiod
飞腾平台最常见的系统就是银河麒麟V10(ARM64版),在联网环境下安装libgpiod很简单:
sudo apt install libgpiod2 libgpiod-dev gpiod但很多国产化项目是内网环境、没有软件源,这时候就只能离线部署。我的做法是:在一台能联网的ARM64机器上下载好deb包(包括libgpiod2、gpiod、libgpiod-dev以及依赖的libc6版本信息),拷贝到目标板子上用dpkg -i安装。如果连deb包都没有,那就需要源码编译:
git clone https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git cd libgpiod ./autogen.sh --enable-tools=yes --prefix=/usr make -j4 sudo make install编译时需要注意,老版本libgpiod依赖autoconf/automake/libtool,缺了哪个configure都会报错。ARM64上编译本身没有坑,内存足够的话直接make就行。装好后gpiodetect能列出chip设备,就说明内核的GPIO子系统已经正常工作。
3.2 用命令行工具先验证引脚编号和电平状态
libgpiod带的工具,是我在这些年做过所有Linux GPIO开发里觉得最趁手的调试利器,比拿万用表去戳引脚高效得多。最常用的四个命令:
gpiodetect # 查看GPIO chip列表 gpioinfo # 查看每个chip的引脚方向、active state、占用者 gpioget <chip> <offset> # 读取某个引脚电平 gpioset <chip> <offset>=1 # 设置某个引脚输出高电平比如确定目标引脚在gpiochip2的第3号偏移:
gpioget gpiochip2 3 gpioset gpiochip2 4=1测量时我会把gpioget、gpioset和示波器配合起来使用:先用gpioset把某个空闲引脚拉高,再看原理图上对应的网络线电平是否变化,这样可以快速确认设备树里引脚编号是否对应正确。如果gpioset设置后电平没变,第一反应不是怀疑GPIO坏了,而是检查这个引脚是否被某个驱动独占(gpioinfo里会显示占用者)或者被pinctrl复用到了其他功能上。
3.3 在C代码里用gpiod库做输入和输出
命令行验证通了之后,就可以上libgpiod库写真正的业务代码了。以读取输入为例:
#include <gpiod.h> #include <stdio.h> int main(void) { struct gpiod_chip *chip; struct gpiod_line *line; int val; chip = gpiod_chip_open_by_name("gpiochip2"); if (!chip) { perror("open gpiochip2 failed"); return -1; } line = gpiod_chip_get_line(chip, 3); if (!line) { perror("get line 3 failed"); gpiod_chip_close(chip); return -1; } if (gpiod_line_request_input(line, "myapp-input") < 0) { perror("request input failed"); gpiod_chip_close(chip); return -1; } val = gpiod_line_get_value(line); printf("GPIO2_3 = %d\n", val); gpiod_line_release(line); gpiod_chip_close(chip); return 0; }输出控制类似,把request_input改成request_output,然后调用gpiod_line_set_value即可。需要特别留意的是gpiod_line_request_*函数的第二个参数,也就是consumer标签,内核的gpio debugfs里会显示这个字符串,如果多个进程都抢同一根引脚,看这个标签就能快速定位是谁在占用。
4. 中断配置:这个项目真正值钱的部分
4.1 为什么强烈不建议用轮询
有些人在GPIO输入上偷懒,用while(1)循环不停gpioget,或者C代码里while读值。小规模测试无所谓,但真正做产品就露馅了:轮询间隔太短,CPU占用率高得离谱;间隔太长,又可能漏掉毫秒级的外部事件。更麻烦的是,轮询天然处理不了“外部事件发生时CPU正在做别的事情”的情况,时延不可控。
飞腾FT2000/4的GPIO是支持边沿中断的,底层通过GIC把中断分发给CPU。正确做法是把中断能力用起来,事件来了内核主动通知应用,应用不需要空转。
4.2 用户态监听中断:gpiomon一行搞定
最快速的验证手段是libgpiod自带的gpiomon:
gpiomon --rising-edge --falling-edge gpiochip2 3把引脚接地再松开,观察终端是否打印事件。如果事件能打印出来,说明硬件链路、中断控制器、GPIO子系统全部正常。实际项目里,我会先用万用表或者信号发生器制造一个明确的边沿,再用gpiomon确认,这样能快速排除板级问题。
4.3 在C代码里用events接口接收中断
用户态程序监听中断的标准做法是:把GPIO line设置为事件模式,然后用gpiod_line_event_wait等待事件发生。下面是完整demo:
#include <gpiod.h> #include <stdio.h> #include <unistd.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("gpiochip2"); if (!chip) return -1; line = gpiod_chip_get_line(chip, 3); if (!line) return -1; /* 请求上升沿和下降沿中断 */ ret = gpiod_line_request_rising_edge_events(line, "myapp-irq"); if (ret < 0) { perror("request rising edge failed"); return -1; } while (1) { ret = gpiod_line_event_wait(line, NULL); if (ret > 0) { if (gpiod_line_event_read(line, &event) == 0) { printf("event type=%d time=%lld.%06lld\n", event.event_type, event.ts.tv_sec, event.ts.tv_nsec / 1000); } } } gpiod_line_release(line); gpiod_chip_close(chip); return 0; }注意gpiod_line_event_wait的第二个参数是超时时间,传NULL表示永久等待。如果要在等待的同时处理其他任务,可以把GPIO line的fd取出来,丢给select或epoll统一管理,这样可以和一个进程里的多个事件源协同工作。gpiod_line_event_get_fd这个接口就是干这个的。
4.4 中断消抖,不能不提的工程问题
机械开关和继电器类外部信号,按下和释放的瞬间会产生毫秒级抖动,如果直接配置上升沿和下降沿中断,一次真实按键可能触发5~10次中断。处理方式不外乎两种:硬件上在按键两端并联RC滤波电容,软件上记录每次中断时间戳,在中断处理里判断“距上次有效事件是否超过N毫秒”,小于阈值直接丢弃。
我在FT2000/4上做按键检测时用的是软件消抖+N秒延时上报的组合方案。中断服务函数里只负责记录当前时间,真正处理事件的线程启动一个20毫秒的定时器,周期内如果收到连续事件就刷新状态,定时器到点后统一上报一次。这个方案能有效过滤抖动,代码也好维护,比单纯的中断里udelay靠谱得多,因为用户态里udelay阻塞还会拖垮整个进程的事件循环。
5. 内核态驱动方案:什么时候必须上,怎么写才稳
5.1 用户态libgpiod不是万能药
你可能想问:既然libgpiod这么爽,还要内核驱动干什么?答案是:用户态方案有硬伤。第一,gpiod_line_event_wait在用户态是阻塞等待,事件发生后从内核到应用层多了一次上下文切换和事件拷贝,时延通常在几十到几百微秒,对某些协议时序要求苛刻的场景不够用;第二,用户态进程可能被杀、被调度延迟,对于工业设备来说不可靠;第三,某些引脚需要在系统启动早期、其他驱动加载前就完成配置,用户态根本来不及。
所以,遇到高实时性、高可靠性的需求,老老实实写内核驱动。FT2000/4上最常用的还是gpio-keys、gpio-leds这类成熟驱动,只有这些不满足时才自己写。
5.2 一个干净的内核驱动骨架
自己写驱动时,我倾向于使用devm(managed device resource)系列的API,好处是资源自动释放,probe失败不会留下垃圾状态。一个读取输入GPIO并注册中断的驱动核心代码如下:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/gpio/consumer.h> #include <linux/interrupt.h> #include <linux/of.h> struct my_gpio_dev { struct gpio_desc *irq_gpio; int irq; }; static irqreturn_t my_gpio_isr(int irq, void *data) { struct my_gpio_dev *dev = data; /* 这里务必快速处理,重活交给下半部或工作队列 */ dev_info(NULL, "GPIO interrupt triggered\n"); return IRQ_HANDLED; } static int my_gpio_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct my_gpio_dev *priv; unsigned long flags = 0; int ret; priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->irq_gpio = devm_gpiod_get(dev, "input", GPIOD_IN); if (IS_ERR(priv->irq_gpio)) { return PTR_ERR(priv->irq_gpio); } priv->irq = gpiod_to_irq(priv->irq_gpio); if (priv->irq < 0) { return priv->irq; } flags = IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING; ret = devm_request_threaded_irq(dev, priv->irq, NULL, my_gpio_isr, flags | IRQF_ONESHOT, "my_gpio_irq", priv); if (ret) { return ret; } platform_set_drvdata(pdev, priv); return 0; } static const struct of_device_id my_gpio_of_match[] = { { .compatible = "vendor,mydevice" }, { } }; MODULE_DEVICE_TABLE(of, my_gpio_of_match); static struct platform_driver my_gpio_driver = { .probe = my_gpio_probe, .driver = { .name = "my_gpio", .of_match_table = my_gpio_of_match, }, }; module_platform_driver(my_gpio_driver); MODULE_LICENSE("GPL");代码里用了devm_request_threaded_irq,这是我很推荐的一种写法:用threaded irq把中断处理自动放到内核线程上下文里,主中断处理函数跑在原子上下文,而线程部分可以调用msleep、gpiod_set_value等可能睡眠的函数。相比传统的request_irq加tasklet或workqueue,代码量少很多,而且不容易写出死锁bug。
5.3 内核态的坑:devm_gpiod_get的命名对应关系
devm_gpiod_get(dev, "input", ...)里的“input”对应设备树里的input-gpios属性,name完全一致才行。如果设备树里写的是irq-gpios,代码里就要写devm_gpiod_get(dev, "irq", ...),少了后半段,返回的一定是ENODEV。这个命名规则经常被忽略,报错后又一脸懵。
6. 高频踩坑记录:中断不触发、读数不对的排查思路
6.1 中断死活不来?先看复用和占用
碰到“代码看着没问题,事件就是不来”的情况,我建议按这个顺序排查:
gpioinfo查看引脚current状态,看看是否被某个driver占用,如果显示"sysfs"或"gpiod"之外的名字,说明有驱动的probe已经占用了这根引脚,你的申请请求会被拒绝或直接拿不到中断号。- 查看
/sys/kernel/debug/gpio(需要先挂载debugfs,或者用mount -t debugfs none /sys/kernel/debug),这里能看到每个引脚的consumer是谁。 - 确认引脚没有复用冲突。很多FT2000/4板子的默认设备树把大多数引脚配置成了UART、I2C等功能,设备树里没有pinctrl-0的情况下,即使你拿它当GPIO申请,硬件引脚的电气连接也可能还在其他外设逻辑上,需要检查原理图确认这个引脚在板级上是否真的连到了你的按键/传感器。
有一次我在客户机器上排查中断不触发,折腾了两小时,最后发现那根引脚原理图上接的是一个I2C设备的SCL,板子上一版固件把它配置成了I2C功能,这版固件虽然改了设备树、把引脚释放出来了,但硬件上还连着一个I2C上拉电阻,导致电平锁死在低电平,永远不出上升沿。所以看中断问题,别光盯软件,硬件万用表示波器该上就上。
6.2 边沿丢失,脉冲太窄怎么办
如果外部信号是几百纳秒级别的脉冲,GPIO控制器可能根本捕获不到,因为GPIO模块的采样时钟没有那么快。遇到这种情况,先查TRM里GPIO输入滤波器的存在性和可配置性。FT2000/4的GPIO不一定有全局的debounce寄存器,可能需要借助芯片的EXTINT模块或者其他输入捕获单元来处理高速信号,这已经不是普通GPIO的范畴了。
普通应用中,如果边沿频率不高,我建议在软件侧加一个简单的环形缓冲区,中断触发后把事件时间戳存入缓冲区,由内核线程单点消费,避免高频中断把系统拖垮。
6.3 ACTIVE_LOW和上拉下拉,别搞混
设备树里GPIO_ACTIVE_HIGH和GPIO_ACTIVE_LOW两个flag直接影响逻辑电平到“1/0”的映射。比如按键一端接GPIO,一端接GND,引脚默认上拉,按下时引脚变低。此时声明GPIO_ACTIVE_LOW后,读取值为1表示“按下”,为0表示“释放”;反过来声明GPIO_ACTIVE_HIGH,读取值就是反的。很多人读按键读数怪异,先检查这里是不是写反了。
另外,FT2000/4的很多引脚内部有可配置上下拉,如果外部没有接电阻,必须确认设备树或者pinctrl中上下拉配置正确。引脚浮空时读数抖动,会表现为“偶尔触发中断”,这个极其恶心。我一般会在原理图阶段就明确每个输入引脚的上下拉方式,软件里再通过pinctrl显式配置,宁可多写两行,不跟硬件扯皮。
6.4 /dev/gpiochip0不存在的内核配置问题
有些精简内核镜像默认没开GPIO子系统,或者没把FT2000/4平台对应的GPIO控制器驱动编进去,导致gpiodetect空手而归。检查内核配置这几项:
CONFIG_GPIOLIB=y CONFIG_GPIO_SYSFS=y (可选,老接口) CONFIG_GPIO_PL061=y (或对应飞腾GPIO控制器驱动) CONFIG_GPIO_CDEV=y (libgpiod依赖的字符设备接口)在飞腾平台上,CONFIG_GPIO_CDEV尤其重要,libgpiod全靠它。如果有/dev/gpiochip0但gpiodetect还是报错,再看看/dev目录权限,有些系统默认/dev/gpiochip*的权限是root-only,普通用户无法访问,需要修改udev规则或者用root执行测试。
最后再分享一个调试技巧
我在飞腾平台上做GPIO调试时,最喜欢用一套“三步验证法”:第一,数据手册和原理图交叉确认ball名、bank、偏移,绝不凭感觉猜编号;第二,在用户态用gpiodetect、gpioinfo把引脚状态摸清楚,配合gpiomon验证中断链路;第三,链路验证通过之后,再切换到最终的libgpiod代码或内核驱动方案。这样三层下来,绝大多数问题都能在用户态阶段暴露出来,不用反复编译内核、重启板子,开发的痛苦程度能下降一个量级。
飞腾FT2000/4的GPIO开发,本质上和任何一款成熟ARM SoC上的GPIO开发没什么区别,核心难点在于对设备树和pinctrl机制的理解,以及拿到不熟悉的板子时如何快速定位引脚关系。把这套方法论打磨出来,以后换到飞腾D2000、或者其他的国产化平台,你也能很快上手。