飞腾D2000 GPIO驱动实战:从设备树到gpiod点灯教程
2026/9/24 5:05:36 网站建设 项目流程

我的办公桌上到现在还留着那块飞腾D2000的评估板,原因很朴素:第一次拿它调GPIO,我花了整整两个晚上。倒不是编译器装不上,也不是板子坏了,而是我仍然带着以前在x86 BSP里写几个寄存器就能点灯的思路,忽略了这套平台上所有硬件资源都要经过设备树(DTS)去“报备”。你要在飞腾D2000上操作一个GPIO,完整链路其实可以拆成四件事:看原理图找引脚、在DTS里声明节点、写驱动代码解析GPIO、最后上板验证。这篇文章就是照着这个链路走的保姆级教程,能帮你少走我当年走过的弯路,尤其适合刚接触飞腾平台、或者从单片机/旧式内核驱动转到Linux设备树体系的开发者。

1. 为什么飞腾D2000上操作GPIO要先碰设备树

1.1 从x86惯性思维到ARM设备树的模式切换

很多从x86平台转过来的朋友,最不适应的就是设备树机制。x86时代,硬件资源基本由BIOS或者固定的BSP在启动阶段安排好,驱动代码里可以直接查表、直接访问固定物理地址。我第一版GPIO驱动就是这么写的:翻芯片手册,找到GPIO控制器基地址,然后ioremap出来直接写寄存器。在飞腾D2000的板子上编译、加载,模块倒是加载成功了,但引脚就是没反应。后来排查才知道,不是寄存器算错,而是引脚复用根本没有配置——这个引脚上电默认是UART功能,不是GPIO功能,我压根没管它。

ARM SoC和x86不太一样,外设密度高、引脚复用关系复杂。同一个物理引脚可能同时连着UART、SPI、PWM、GPIO控制器好几路内部信号,如果每个驱动都在C代码里硬编码引脚号和复用寄存器,那换一块板子就要动一片代码,厂商也没法维护。设备树的价值在于:把“硬件长什么样、外设接在哪儿、引脚复用到哪个功能”统一放进一个独立文本里,内核启动时解析这份文本,驱动只负责向内核要资源,不关心资源具体在物理地址的哪个角落。所以飞腾D2000上,写GPIO驱动的第一课不是看GPIO控制器寄存器,而是先学会看DTS、写DTS。

1.2 从DTS到DTB再到驱动的完整数据流

理解数据流对排查问题帮助很大,我按顺序说一下:

  1. 设备树源文件分为.dtsi.dts.dtsi通常描述SoC内部的通用内容,比如CPU、中断控制器、GPIO控制器的存在与地址;.dts描述某块具体板卡上的外设、引脚、设备连接关系。
  2. 这两个文件经过设备树编译器dtc,编译成二进制.dtb。这块dtb由bootloader在启动时拷贝到内存,并把物理地址传给内核。
  3. 内核解析dtb,把每个节点展开成device_node。其中带compatible属性的节点,会由平台总线注册成platform_device
  4. 驱动通过of_match_table注册自己支持的compatible字符串。两边对上了,内核调用驱动的probe函数。
  5. 驱动在probe里调用gpiod_get一类的API,从设备树节点中读取xxx-gpios属性,得到一个GPIO描述符,然后就能用gpiod_set_value去操作电平了。

这个链条里最容易出问题的是第4步和第5步:compatible字符串对不上,或者GPIO属性名写错。后面我会专门讲排查方法。

1.3 动手前先备齐资料

写代码之前,建议先把这些东西准备好,不然后面会到处卡壳:

  • 飞腾D2000板卡的BSP内核源码,重点看arch/arm64/boot/dts/phytium/目录下的dts和dtsi文件。
  • 板卡原理图PDF。不要只盯着芯片手册看寄存器,先看板子上LED、按键、排针到底连到了D2000的哪个引脚。
  • 一套能用的串口或SSH通道,因为调试时dmesg几乎是必看的。
  • 设备树反编译工具dtc,以及GPIO调试工具libgpiod(包含gpiosetgpioinfogpioget等命令)。
  • U-Boot环境下加载内核和dtb的方法,改完dts后总归要让新dtb生效。

我第一次调飞腾D2000的GPIO时,手边连原理图都没有,全靠猜,结果一个方向性配置猜了一下午。这个教训挺深刻。

2. 先看硬件原理图再开工:确认引脚、bank和复用关系

2.1 引脚编号没有魔法,都在原理图上

设备树里写<&gpio0 12>这个数字,不是随便找的,它必须和原理图上芯片引脚的GPIO编号一一对应。飞腾D2000的GPIO并不是一个超级大控制器,通常分成多个bank/控制器,每个控制器管理连续的几十个引脚。在BSP的dtsi里,你会看到类似这样的节点:

gpio0: gpio@28000000 { compatible = "phytium,ftd2000-gpio"; reg = <0x0 0x28000000 0x0 0x1000>; gpio-controller; #gpio-cells = <2>; };

这就是一个GPIO控制器节点,gpio0是它的标签。假如原理图上某引脚写的是GPIO_12,在dts里就引<&gpio0 12>。如果是第二个控制器对应的引脚,比如GPIO_45,那要先查第二个控制器的引脚编号映射关系,通常写成<&gpio1 13>这种减去bank基址的形式。每个BSP的bank划分不一定完全一样,我见过有的把0~31划为gpio0,有的把0~15划为gpio0,所以绝对不能照着网上的示例硬套,一定要去BSP的dtsi里看gpio控制器节点定义。

2.2 pinctrl:一个物理引脚的多重身份

原理图上经常能见到类似GPIO_12 / UART1_TXD这种标注,意思是这个引脚既能当GPIO,又能当串口发送脚。芯片内部有个MUX逻辑,决定这个引脚当前接给哪个外设。pinctrl子系统就是Linux内核里负责管这个MUX的。

理解pinctrl只需要一个类比:引脚是一个多面手员工,既会写代码又会画板子,pinctrl就是HR,它决定今天这个员工去干哪份工作。DTS里的pinctrl-0 = <&led_pin>就是在给HR下指令:把某个引脚的状态切成GPIO模式。

如果这个引脚已经被别的外设占用,GPIO驱动去申请时,很可能会得到一个busy错误,甚至会更隐蔽——申请成功了,但引脚电平始终变不了。所以拿到板子后,第一件事是在dtsi里搜一下目标引脚有没有被其他设备引用。

2.3 别忽略外部电路,极性是GPIO的第一道坎

GPIO操作看起来是软件问题,但一半的坑在硬件电路。比如接一个LED,如果LED阳极接GPIO、阴极通过电阻接GND,那这个灯是高电平点亮;反过来,如果LED阳极接VCC、阴极通过电阻接GPIO,那就是低电平点亮。这两种接法在设备树里对应的极性标志完全不同,前者是GPIO_ACTIVE_HIGH,后者是GPIO_ACTIVE_LOW

很多初学者不理解为什么GPIO还要分逻辑电平和物理电平。简单说,gpiod_set_value(desc, 1)代表的是“逻辑上的1”,也就是“激活”状态。如果dts里标了GPIO_ACTIVE_LOW,那么逻辑1会让物理引脚输出低电平。这样写驱动的最大好处是更换板卡时,只需要改dts,驱动代码不用管LED是哪种接法。这个特性后面还会提,因为它是新旧API最大的区别之一。

3. DTS具体怎么写:从GPIO控制器到自定义外设节点

3.1 先理解GPIO控制器节点的两个关键声明

在BSP的dtsi里,GPIO控制器节点通常长这样:

gpio0: gpio@28000000 { compatible = "phytium,ftd2000-gpio"; reg = <0x0 0x28000000 0x0 0x1000>; interrupts = <GIC_SPI 12 IRQ_TYPE_LEVEL_HIGH>; gpio-controller; #gpio-cells = <2>; };

关键点有两个。gpio-controller;是告诉内核“我是一个GPIO提供者”,可以给其他设备提供GPIO资源。#gpio-cells = <2>是告诉后面的引用方:“你要引用我的GPIO,需要附带2个参数”。第一个参数是bank内的引脚编号,第二个参数是极性标志,用GPIO_ACTIVE_HIGHGPIO_ACTIVE_LOW,分别对应0和1。

这种属性命名方式在中断控制器里也一样,比如#interrupt-cells = <3>。它们是设备树的一种“接口描述”,就像函数的参数列表。你写<&gpio0 12 GPIO_ACTIVE_HIGH>时,其实就是在调用“gpio0这个控制器提供的、第12号、高电平激活”这个GPIO。

3.2 写一个能被驱动认出来的自定义外设节点

假设我们想通过GPIO控制一个LED,不想改内核其它驱动,就在板级dts里新建一个自定义节点,名字随意,但compatible要有辨识度,通常格式是“厂商,设备名”。示例:

/dts-v1/; #include <dt-bindings/gpio/gpio.h> / { demo-led { compatible = "demo,led"; led-gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>; pinctrl-names = "default"; pinctrl-0 = <&led_pin>; }; };

这里有个特别容易踩的坑:led-gpios这个属性名不是随便起的。驱动里如果用devm_gpiod_get(dev, "led", GPIOD_OUT_LOW)来获取GPIO,内核会自动补上-gpios后缀,去寻找名为led-gpios的属性。所以dts属性名和驱动代码里的conid必须严格对应。如果你想一次获取多个GPIO,可以分别叫led-gpiospower-gpiosrst-gpios,驱动里用devm_gpiod_get(dev, "power", ...)依次获取,非常清晰。

3.3 pinctrl复用节点怎么写才不会被卡住

上面dts片段里的pinctrl-0 = <&led_pin>引用了led_pin这个节点,这个节点通常定义在pinctrl控制器下面。通用写法类似:

&pinctrl { led_pin: led-pin { pins = "GPIO_12"; function = "gpio"; }; };

注意,pinsfunction这两个字段的语法在不同厂商的pinctrl驱动里不完全一致。飞腾D2000的BSP可能有自己的一套写法,比如数值型pinmux配置。我建议的做法是:先查看当前dtsi里有没有现成的pinctrl配置节点,有的话直接模仿;如果没有,并且板子上电后引脚默认就是GPIO功能,那可以先不写pinctrl-0,把灯点起来再说,避免一开始就被复用问题卡住。

3.4 改完DTS怎么让它生效

修改dts之后,我一般直接用内核的dtb编译命令:

make dtbs

然后把新编译出来的dtb替换到/boot或者U-Boot实际加载的分区。不同BSP的烧写方式不同,有的用tftp,有的直接cp到某个ext4分区,这个要看你板卡的BSP文档。

有个调试技巧很实用:用dtc反编译当前生效的dtb,看内核到底收到了什么内容:

dtc -I dtb -O dts -o current.dts /boot/xxx.dtb

然后对比源码里的dts,就能确认自己改的节点有没有真正编译进去。我遇到过好几次这种情况:改了半天驱动不生效,最后发现U-Boot加载的还是旧dtb,反编译一看,节点根本不存在。

4. 驱动代码全解:probe如何拿到GPIO并控制电平

4.1 platform_driver的匹配原理

设备树里非memory-mapped的自定义设备,会被注册为platform_device。对应的驱动就是一个platform_driver。你要做的第一件事是把of_match_table填对:

static const struct of_device_id led_of_match[] = { { .compatible = "demo,led" }, { } }; MODULE_DEVICE_TABLE(of, led_of_match);

这个compatible字符串必须和dts节点里的完全一致,多一个空格、大小写不同都不行。内核在注册设备后,会用这个表去匹配节点;匹配成功,就调用驱动的probe函数。 很多第一次写的人,probe不执行,十有八九是这里不一致。我自己的排查技巧是:加载模块后去/sys/bus/platform/devices/下看有没有生成对应的设备目录,再在/sys/bus/platform/drivers/demo_led/下看驱动是否bind上设备。如果设备存在但没bind,就去核对compatible。

4.2 新API gpiod和旧API之间的选择

在写GPIO驱动时,前辈们的代码里经常出现这类旧API:

int gpio = of_get_named_gpio(np, "led-gpios", 0); gpio_request(gpio, "led"); gpio_direction_output(gpio, 1); gpio_set_value(gpio, 1);

这套API在老的4.x内核里很常见,缺点也很明显:拿到的是全局GPIO号,要手动gpio_request,用完后还要手动gpio_free;而且在操作时绕过active-low语义,逻辑和物理电平混在一起,代码移植到不同板卡时很容易出问题。新内核强烈建议用gpiod系列API,也就是GPIO描述符接口:

data->led = devm_gpiod_get(dev, "led", GPIOD_OUT_LOW); gpiod_set_value(data->led, 1);

devm_gpiod_get的好处是:驱动卸载时GPIO会被自动释放,不用手动操心;它会自动解析dts里的GPIO_ACTIVE_LOW,代码里只表达逻辑状态;返回的struct gpio_desc *和具体引脚号完全解耦。新项目一律建议用这套。

4.3 一个完整可编译的示例驱动

下面这个模块是我实际调通过的写法,删减了业务逻辑,只保留核心链路。功能是在probe时让LED连续翻转三次,然后停在灭的状态,用来验证GPIO通路是否打通。

#include <linux/module.h> #include <linux/platform_device.h> #include <linux/of.h> #include <linux/gpio/consumer.h> #include <linux/delay.h> struct reg_led_data { struct gpio_desc *led; }; static int led_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct reg_led_data *data; int ret, i; data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >obj-m := demo_led.o KDIR := /path/to/phytium-kernel-source ARCH := arm64 CROSS_COMPILE := aarch64-linux-gnu- all: $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) clean

KDIR指向飞腾D2000的内核源码目录,注意编译模块时不需要先编译整个内核,只需要内核源码已经配置过、并且生成了必要的头文件和Module.symvers。如果你直接在板子上编译,也可以把KDIR改成/lib/modules/$(shell uname -r)/build,同时去掉ARCH和CROSS_COMPILE,前提是板子的文件系统里装了内核头文件包。

这里回应一个网上经常搜到的问题:驱动代码到底放在哪个目录?答案是,调试阶段完全没必要放进内核源码树的drivers目录。用外部模块方式,一个独立目录放.cMakefile就能编,改代码、加载、卸载非常灵活。等驱动稳定了,再考虑合并进BSP的drivers目录作为内核内建驱动。

5. 上板验证:insmod、debugfs和libgpiod三板斧

5.1 insmod之后先看dmesg,不是先看灯

很多人把模块拷到板子上,insmod demo_led.ko之后第一反应是盯着LED看,这其实不太科学。我习惯先看内核日志:

insmod demo_led.ko dmesg | tail -20

正常能看到类似的输出:

demo-led demo-led: demo led probe ok

如果没有任何输出,说明probe可能根本没跑。这时去/sys/bus/platform/devices/下看有没有名为demo-led的设备目录。如果有设备但没bind,就去检查of_match_table;如果设备目录都不存在,说明内核解析dts时就没生成这个platform device,重点检查dts节点有没有编进dtb。

5.2 debugfs:查看GPIO状态的利器

看GPIO当前状态,最直接的是内核的debugfs接口。先挂载:

mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/gpio

输出里能看到gpiochip0、gpiochip1这类控制器信息,以及每一条线当前被谁占用、方向、电平。比如我驱动加载后,能看到类似gpio-12 ( |demo-led ) out hi的行,说明GPIO被我们的驱动占用,方向是输出,电平是高。注意,不同内核版本这一行的格式略有差异,但核心信息不变。

如果卸载模块,这行占用信息会消失,GPIO被释放回系统。利用这个接口,可以快速判断“引脚到底被谁占用了”,排查力度远高于肉眼测电平。

5.3 /proc/device-tree:确认内核实际解析到的节点内容

我还有一个习惯,就是看/proc/device-tree/。它把内核真实解析到的设备树节点以目录形式暴露出来:

ls /proc/device-tree/demo-led cat /proc/device-tree/demo-led/compatible

cat compatible会输出两个以空字符结尾的字符串,比如demoled。如果这个目录缺失,说明dts节点没进内核;如果存在,说明设备树解析这一层是通的。这个方法对排查“我明明改了dts为什么不生效”这类问题特别有效。

5.4 用libgpiod工具反向验证硬件

如果怀疑驱动代码写错了,但又想确认硬件和接线没问题,可以用libgpiod工具直接操作GPIO,把“驱动问题”和“硬件问题”切分开:

gpioinfo gpioset gpiochip0 12=1

前提是先把驱动模块卸载,让GPIO释放出来。gpioset执行后,用万用表测引脚电平,如果电平跟着变,说明硬件链路没问题,问题出在驱动侧;如果电平不变,就要回到硬件或者pinctrl复用上找原因。这一招是我排查GPIO问题最常用的一种隔离手段。

5.5 旧版sysfs接口的补充说明

如果BSP内核还保留着/sys/class/gpio,也可以用它快速测试:

echo 12 > /sys/class/gpio/export echo out > /sys/class/gpio/gpio12/direction echo 1 > /sys/class/gpio/gpio12/value

但需要注意两点:一是新内核逐渐移除这个接口,建议优先使用libgpiod;二是sysfs里的编号是“全局GPIO号”,不是dts里的bank内编号,要先通过debugfs确认gpiochipX的base值,再计算实际编号。比如gpiochip0的base是0,那dts里<&gpio0 12>对应的全局号就是12。这个换算关系容易搞错,我吃过一次亏。

6. 按排查顺序整理:GPIO调不通时先查哪里

6.1 probe没触发的完整检查链路

如果加载驱动后完全没有probe日志,我会按下面顺序查:

检查项方法
dts节点是否存在ls /proc/device-tree/看对应节点目录
compatible是否一致对比dts和of_match_table字符串,空格大小写都要一致
dtb是否更新dtc -I dtb -O dts反编译当前dtb,搜节点名
设备是否被bindls /sys/bus/platform/drivers/demo_led/是否有设备号
设备是否被disabledts节点里有没有status = "disabled",有的话改成okay

这个流程走完,绝大多数probe不执行的问题都能定位。

6.2 gpiod_get返回各种错误码怎么办

devm_gpiod_get失败时会返回ERR_PTR,最常见的是这几个:

错误码dmesg显示常见原因
-22-EINVAL#gpio-cells数量不对,或第三个参数方向枚举值非法
-2-ENOENT找不到led-gpios属性,多半是属性名和后缀不匹配
-16-EBUSYGPIO已经被其他设备占用,查pinctrl或其它驱动
-517-EPROBE_DEFERGPIO控制器还没ready,这是正常的依赖等待

-EPROBE_DEFER很多人第一次见会慌,其实它不是错误,是内核在告诉你“这个GPIO的提供者还没注册好,我把这个值返回出去,你晚点再probe我”。所以在probe里碰到这个返回值,直接return PTR_ERR(data->led)就行,内核会延后重试。千万不要在这个值上做特殊处理或者转成其它错误码,否则可能永远等不到下一次probe。

6.3 电平死活不变:先查复用,再查极性,最后查电路

最常见的一种诡异现象是:驱动probe成功、GPIO申请也成功、gpiod_set_value也调了,但万用表量到引脚电平纹丝不动。我会按这个顺序排查:

  1. 查pinctrl复用。看dmesg有没有类似pinmux ... cannot be claimed的报错,或者直接搜dts里这个引脚是否被其它节点配置成了串口、SPI功能。
  2. 查极性配置。确认原理图LED或外设是低电平有效还是高电平有效,再看dts里是GPIO_ACTIVE_LOW还是GPIO_ACTIVE_HIGH。如果电路是低电平点亮,而你标成ACTIVE_HIGH,那么gpiod_set_value(desc, 1)得到的是低电平,灯当然不亮——但这个逻辑是“不亮”还是“亮”,取决于你怎么定义。
  3. 查实际焊接。用万用表测量芯片引脚端和排针端是否都正常,排除断线、虚焊。

很多“GPIO不工作”的问题,最后是出现在这两点:复用被抢,或者极性反了。我见过有人在同一个引脚上花了半天,最后发现引脚被音频驱动给用了。

6.4 中断引脚要特别注意触发类型

如果GPIO是用作按键中断,不要指望dts里的GPIO_ACTIVE_LOW能代替中断触发类型。获取中断的常见写法是:

int irq = gpiod_to_irq(data->key); ret = request_threaded_irq(irq, NULL, key_isr, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, "demo-key", data);

在飞腾D2000这类平台上,GPIO中断也要确认引脚对应的中断控制器配置正确。有时候你会遇到gpiod_to_irq返回一个看起来正常的中断号,但触发条件完全不生效,那种情况多半要和dts里的interrupt-parentinterrupts属性一起核对。一个容易忽视的点:如果按键按下被拉低,需要确认电路里有没有外部上拉电阻,以及dts里有没有配置成默认高电平。软件和硬件在这里是一套完整链路,缺一个环节都不行。

6.5 主线内核和BSP差异:移植驱动的必修课

最后提醒一个长期项目会遇到的问题:飞腾D2000相关的驱动和dts在主线内核里能搜到,但不同版本的节点命名、GPIO控制器数量、pinctrl驱动能力,和厂商发布的BSP会有差异。比如某BSP里GPIO控制器叫gpio0gpio1,到了版本更新的主线内核可能改成了带地址后缀的节点名,或者GPIO中断号变了。所以你搜到网上参考代码时,不要迷信,第一步先看当前内核的使用习惯,以手上的BSP为准。这也是为什么我反复强调要用dtc反编译当前生效的dtb,那才是板上实际运行的真相。

我自己现在拿到任何一块新板子,调GPIO之前一定会先做两件事:反编译当前dtb,搜目标引脚;再查/sys/kernel/debug/gpio看有没有被占用。这俩动作加起来不到五分钟,却能过滤掉至少一半的“GPIO不听话”问题。另外,如果你在调试时看到-517,请放心大胆地把它原样返回给内核,那不是崩溃,也不是你写错了,只是内核在按正常依赖顺序等待GPIO控制器就位而已。这套流程走熟了,飞腾D2000上的GPIO开发,对你来说就和一个简单的LED点灯工程没什么区别了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询