飞腾D2000 GPIO驱动实战:从设备树到gpiod点灯教程
我的办公桌上到现在还留着那块飞腾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再到驱动的完整数据流理解数据流对排查问题帮助很大我按顺序说一下设备树源文件分为.dtsi和.dts。.dtsi通常描述SoC内部的通用内容比如CPU、中断控制器、GPIO控制器的存在与地址.dts描述某块具体板卡上的外设、引脚、设备连接关系。这两个文件经过设备树编译器dtc编译成二进制.dtb。这块dtb由bootloader在启动时拷贝到内存并把物理地址传给内核。内核解析dtb把每个节点展开成device_node。其中带compatible属性的节点会由平台总线注册成platform_device。驱动通过of_match_table注册自己支持的compatible字符串。两边对上了内核调用驱动的probe函数。驱动在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包含gpioset、gpioinfo、gpioget等命令。U-Boot环境下加载内核和dtb的方法改完dts后总归要让新dtb生效。我第一次调飞腾D2000的GPIO时手边连原理图都没有全靠猜结果一个方向性配置猜了一下午。这个教训挺深刻。2. 先看硬件原理图再开工确认引脚、bank和复用关系2.1 引脚编号没有魔法都在原理图上设备树里写gpio0 12这个数字不是随便找的它必须和原理图上芯片引脚的GPIO编号一一对应。飞腾D2000的GPIO并不是一个超级大控制器通常分成多个bank/控制器每个控制器管理连续的几十个引脚。在BSP的dtsi里你会看到类似这样的节点gpio0: gpio28000000 { 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: gpio28000000 { 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_HIGH或GPIO_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-gpios、power-gpios、rst-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; }; };注意pins和function这两个字段的语法在不同厂商的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驱动时前辈们的代码里经常出现这类旧APIint 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) cleanKDIR指向飞腾D2000的内核源码目录注意编译模块时不需要先编译整个内核只需要内核源码已经配置过、并且生成了必要的头文件和Module.symvers。如果你直接在板子上编译也可以把KDIR改成/lib/modules/$(shell uname -r)/build同时去掉ARCH和CROSS_COMPILE前提是板子的文件系统里装了内核头文件包。这里回应一个网上经常搜到的问题驱动代码到底放在哪个目录答案是调试阶段完全没必要放进内核源码树的drivers目录。用外部模块方式一个独立目录放.c和Makefile就能编改代码、加载、卸载非常灵活。等驱动稳定了再考虑合并进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/compatiblecat compatible会输出两个以空字符结尾的字符串比如demo和led。如果这个目录缺失说明dts节点没进内核如果存在说明设备树解析这一层是通的。这个方法对排查“我明明改了dts为什么不生效”这类问题特别有效。5.4 用libgpiod工具反向验证硬件如果怀疑驱动代码写错了但又想确认硬件和接线没问题可以用libgpiod工具直接操作GPIO把“驱动问题”和“硬件问题”切分开gpioinfo gpioset gpiochip0 121前提是先把驱动模块卸载让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也调了但万用表量到引脚电平纹丝不动。我会按这个顺序排查查pinctrl复用。看dmesg有没有类似pinmux ... cannot be claimed的报错或者直接搜dts里这个引脚是否被其它节点配置成了串口、SPI功能。查极性配置。确认原理图LED或外设是低电平有效还是高电平有效再看dts里是GPIO_ACTIVE_LOW还是GPIO_ACTIVE_HIGH。如果电路是低电平点亮而你标成ACTIVE_HIGH那么gpiod_set_value(desc, 1)得到的是低电平灯当然不亮——但这个逻辑是“不亮”还是“亮”取决于你怎么定义。查实际焊接。用万用表测量芯片引脚端和排针端是否都正常排除断线、虚焊。很多“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-parent、interrupts属性一起核对。一个容易忽视的点如果按键按下被拉低需要确认电路里有没有外部上拉电阻以及dts里有没有配置成默认高电平。软件和硬件在这里是一套完整链路缺一个环节都不行。6.5 主线内核和BSP差异移植驱动的必修课最后提醒一个长期项目会遇到的问题飞腾D2000相关的驱动和dts在主线内核里能搜到但不同版本的节点命名、GPIO控制器数量、pinctrl驱动能力和厂商发布的BSP会有差异。比如某BSP里GPIO控制器叫gpio0、gpio1到了版本更新的主线内核可能改成了带地址后缀的节点名或者GPIO中断号变了。所以你搜到网上参考代码时不要迷信第一步先看当前内核的使用习惯以手上的BSP为准。这也是为什么我反复强调要用dtc反编译当前生效的dtb那才是板上实际运行的真相。我自己现在拿到任何一块新板子调GPIO之前一定会先做两件事反编译当前dtb搜目标引脚再查/sys/kernel/debug/gpio看有没有被占用。这俩动作加起来不到五分钟却能过滤掉至少一半的“GPIO不听话”问题。另外如果你在调试时看到-517请放心大胆地把它原样返回给内核那不是崩溃也不是你写错了只是内核在按正常依赖顺序等待GPIO控制器就位而已。这套流程走熟了飞腾D2000上的GPIO开发对你来说就和一个简单的LED点灯工程没什么区别了。