在嵌入式开发圈里,树莓派的GPIO几乎成了"开箱即用"的代名词:装个RPi.GPIO库,几行代码点灯、读传感器,舒舒服服。但换成Jetson Orin Nano,很多人的第一反应是懵——官方文档绕来绕去,一会儿说用设备树,一会儿说查Pinmux,Python和C++还各有各的库。我刚开始上手的时候也被绕晕过,踩了几个坑之后才把整套逻辑理顺。
这篇东西就把我这段时间在Orin Nano上折腾GPIO的完整经验写出来。核心就一件事:把Pinmux引脚复用机制吃透,然后分别用Python和C++把GPIO真正用起来。不要以为点个灯就算会了,引脚复用配置才是Orin Nano和树莓派在GPIO使用上最大的分水岭。搞懂它,后面接传感器、接串口、接SPI屏幕都不慌。
1. Pinmux不是摆设:Orin Nano的GPIO为什么不能像树莓派那样"开箱即用"
1.1 从树莓派到Orin Nano,硬件逻辑发生了什么变化
树莓派上的40Pin接口,每一个物理引脚的功能基本是固定的。你查一下引脚图,上面写着GPIO17就是GPIO17,写着I2C就是I2C,不需要额外配置。原因在于博通的SoC在设计时,已经把每个引脚的功能在出厂时就绑定死了一部分,留给用户的只有那么几个复用选项。
Jetson Orin Nano完全不同。它的40Pin扩展头引脚,很多都是多路复用的。同一个物理引脚,既可以是GPIO,也可以是UART的TX/RX,还可以是I2C的SCL/SDA,甚至可以是SPI的MOSI。具体选哪个功能,由芯片内部的Pinmux寄存器决定。也就是说,在你写Python代码之前,得先让硬件知道"我这个引脚现在要当GPIO用",否则后面全是白搭。
这就是Pinmux(Pin Multiplexing)存在的意义。名字听着高大上,其实逻辑上和家里的墙壁开关差不多:同一个灯口,你接上白炽灯它就是照明,接上智能灯泡它就是智能设备,但前提是你得把开关拨到对应的档位。
1.2 Orin Nano引脚组的分布规律,不看芯片手册也能记住
Orin Nano的40Pin扩展头,物理引脚编号从1到40。我一开始习惯性去找树莓派那种"GPIO编号=物理引脚号"的对应关系,结果发现完全对不上。Orin Nano用的是SoC内部的GPIO控制器编号,而且分成了好几组(GPIO0到GPIO7之类),每组下面又有多个bank。
这里有个很关键的规律:40Pin头里可用的GPIO,基本集中在特定几个控制器下的特定bank。比如很常用的引脚29,它对应的是GPIO3组的06号,在系统里记作gpio-114(具体编号跟内核版本和DTB配置有关)。与其死记硬背,不如记住一条实操经验:
拿到板子之后,第一步就去读
/sys/kernel/debug/gpio或者用gpioinfo工具列出当前所有GPIO的状态,比对着40Pin丝印图来看,比看一百页芯片手册都高效。
后面我会给一个完整脚本,直接帮你把"物理引脚 → 芯片GPIO号 → 系统gpiochip编号"一次查清楚。
1.3 一个容易被忽略的坑:不同型号的Orin Nano,默认配置都不一样
这一点我在网上看到很多人中招。Jetson Orin Nano有8GB和16GB两个版本,开发套件(DevKit)和第三方的载板(比如各种工业核心板)默认的DTB配置是不同的。同一个引脚,在官方开发板上出厂已经配成了GPIO功能,但在某些第三方载板上,出厂默认可能是UART或者I2C。
所以,任何教你"按这个引脚号直接写代码"的文章,都隐含了一个前提:DTB里的Pinmux配置已经是GPIO模式。如果你的板子不是默认配置,或者你买的是第三方载板,第一步要做的就是复核引脚复用状态。这也是为什么我坚持把Pinmux放在第一章讲——它决定了你后面写的是"能跑的代码"还是"报错的代码"。
2. 开工前的准备:环境搭建、工具链与权限三道门槛
2.1 查看当前Pinmux配置:用官方工具摸清家底
Orin Nano刷完JetPack SDK之后,系统里自带一个很好用的工具:/opt/nvidia/jetson-io/config-by-pin.py。它本质上是一个基于Python的配置脚本,用来修改设备树Overlay来调整40Pin头引脚的功能。
建议上手第一件事,先跑一遍:
sudo /opt/nvidia/jetson-io/config-by-pin.py -l这个命令会列出当前40Pin头上每个引脚的复用状态:是GPIO、UART、I2C、SPI、I2S还是PWM。如果你发现某个你想用的引脚现在不是GPIO状态,就需要用这个工具去生成对应的Overlay DTB,然后在/boot/extlinux/extlinux.conf里指定加载它。
不过,我更推荐先用config-by-pin.py把状态摸清楚,然后看引脚是否需要改动。如果只是做GPIO实验,官方开发板默认已经有相当一部分引脚处于GPIO模式了,可以不急着改设备树。
2.2 安装Python扩展库:别用系统自带的那套旧库
Jetson平台上的Python GPIO库,最主流的两个选择是Pigpio(对Jetson支持一般,主要是树莓派生态)和Jetson.GPIO(英伟达官方维护,API风格和RPi.GPIO几乎一样)。我强烈推荐用Jetson.GPIO,理由后面会讲,这里先说环境搭建。
sudo apt update sudo apt install python3-pip sudo pip3 install Jetson.GPIO装完之后有一个非常关键的操作:Jetson.GPIO默认拒绝用root权限以外的用户直接访问GPIO,除非你把当前用户加入gpio组:
sudo groupadd -f gpio sudo usermod -aG gpio $USER然后还需要给gpio组对应的udev规则授权,官方仓库里有一条规则文件,直接复制到udev目录:
sudo cp /opt/nvidia/jetson-gpio/etc/99-gpio.rules /etc/udev/rules.d/ sudo udevadm control --reload-rules sudo udevadm trigger做完这些,记得注销重新登录,让组权限生效。我一直觉得这个设计挺反直觉的:你明明有sudo,但如果不做这一步,Python脚本调库的时候会直接抛PermissionError,而且报错信息不长,很容易忽略根因。
2.3 C++开发要用到的工具链与libgpiod
C++这边底层推荐直接用libgpiod。这是Linux内核官方GPIO子系统在用户空间的C库和工具集,比当年老掉牙的sysfs接口正规太多。JetPack里很贴心地预装了libgpiod的命令行工具(gpiodetect、gpioinfo、gpioget、gpioset),但开发用的头文件和库不一定齐全,保险起见手动装:
sudo apt install libgpiod-dev gpiod编译的时候只需要链接gpiod库:
g++ -o gpio_test gpio_test.cpp -lgpiod装好之后用gpiodetect看一下系统里的GPIO控制器:
gpiodetect正常会输出类似:
gpiochip0 [tegra-gpio] (252 lines) gpiochip1 [tegra-gpio-aon] (32 lines) gpiochip2 [tegra-gpio-ext] (24 lines)这三个chip分别对应不同的控制器域,用的时候要搞清楚哪个chip对应40Pin扩展头。不同版本的JetPack可能chip编号不同,以你实际输出为准。
2.4 用两行代码验证通路:把第一个引脚查出来
在正式开始点灯之前,我建议大家先花30秒做一个通路验证,确认你的环境和工具是通的。用gpioinfo查看某个chip下所有引脚状态:
sudo gpioinfo gpiochip0你会看到类似line 6: "PCA09" [unused]这样的输出。如果某个引脚当前被复用为GPIO且状态是unused,说明你可以操作它。如果显示[used]或者压根没列出,说明它被其他功能占用了,需要回去改Pinmux配置。
这一步不是浪费时间。我见过太多人上来就点灯,结果灯不亮,折腾了半天发现引脚在DTB里被配置成UART了,代码再对也没用。
3. Python实战:用Jetson.GPIO在3分钟内点亮第一颗LED
3.1 为什么选Jetson.GPIO而不是其他Python库
Jetson.GPIO是英伟达官方维护的库,API几乎照搬RPi.GPIO。我之所以推荐它,原因有三:
- 迁移成本极低:用惯树莓派RPi.GPIO的人,看Jetson.GPIO文档基本不用动脑子,模式、引脚编号、读写API基本一致。
- 官方维护:JetPack版本更新后,这个库会同步适配硬件层变化,不用担心内核升级把库搞挂。
- 带版本检查:它会在运行时校验当前板卡型号,防止误在非Jetson设备上跑。
3.2 接线:搞清物理引脚和GPIO号的映射
我这里用一个最经典的点灯实验。LED正极串联一个220Ω限流电阻接到40Pin头的第29脚(物理引脚29),LED负极接第30脚(GND)。
问题来了,怎么知道物理第29脚在Jetson.GPIO里叫什么?这就是新手最容易卡住的地方。Jetson.GPIO使用的命名规则不是物理引脚号,也不是GPIO控制器的绝对编号,而是以40Pin扩展头名称为基准的板载编号,比如board引脚29对应库里的JETSON_ORIN_NANO_40_PIN_HEADER_PIN29这种常量。
实际写代码时看这个就够用了:
import Jetson.GPIO as GPIO # 指定板卡类型,确保引脚映射正确 GPIO.setmode(GPIO.BOARD)GPIO.BOARD模式下,引脚编号直接用40Pin头的物理引脚号,跟我这种习惯了物理引脚图的人最友好。不需要去查GPIO控制器里的数字编号,那是驱动层操心的事。
3.3 第一个Python点灯脚本,以及背后的初始化顺序
直接上代码:
import Jetson.GPIO as GPIO import time # 指定为Orin Nano开发板(按照实际板型选择) GPIO.setmode(GPIO.BOARD) GPIO.setwarnings(False) LED_PIN = 29 # 物理引脚29 GPIO.setup(LED_PIN, GPIO.OUT, initial=GPIO.LOW) try: while True: GPIO.output(LED_PIN, GPIO.HIGH) time.sleep(0.5) GPIO.output(LED_PIN, GPIO.LOW) time.sleep(0.5) except KeyboardInterrupt: pass finally: GPIO.cleanup()这里有两个细节值得说:
initial=GPIO.LOW很关键。如果不显式指定初始电平,有些情况下引脚会保持上一次的残留状态,灯可能一上电就亮,容易造成误判。GPIO.cleanup()不只是清理Python侧的状态,它会把引脚恢复成默认输入模式,避免程序退出后引脚仍保持高电平,把后面的实验带偏。
跑这个脚本时,如果灯不亮,不要急着改代码,先按这个顺序排查:引脚29是不是被DTB配置成了GPIO?用的GPIO.BOARD还是GPIO.BCM?如果强行用BCM(树莓派习惯),引脚号对不上,灯永远不亮。
3.4 不只是点灯:用Python读取按钮输入的完整姿势
点灯只是输出,GPIO的另一半是输入。我经常用GPIO读按钮或者读传感器的高低电平信号,这个时候初始化稍微有点讲究。
比如把一个按钮一端接在物理第31脚(GPIO),另一端接GND,利用内部上拉电阻读取状态:
import Jetson.GPIO as GPIO import time GPIO.setmode(GPIO.BOARD) BTN_PIN = 31 GPIO.setup(BTN_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) try: while True: if GPIO.input(BTN_PIN) == GPIO.LOW: print("Button pressed") else: print("Button released") time.sleep(0.1) except KeyboardInterrupt: pass finally: GPIO.cleanup()这里设置pull_up_down=GPIO.PUD_UP是让引脚启用内部上拉电阻。这样按钮没按下时引脚读到高电平,按下接地后读到低电平,不需要外接上拉电阻,电路可以简化很多。
对于Orin Nano这样引脚内阻不算低的平台,内部上拉大概在几十kΩ级别,驱动继电器这类大负载肯定不行,但读按钮、读干接点信号完全够用。
4. C++实战:用libgpiod直通底层,告别Python的权限限制
4.1 C++不是复杂,是绕开了很多麻烦
Python那套适合快速验证,但实际项目里我倾向于用C++,尤其是做工业控制、数据采集这种对稳定性和权限有要求的场景。C++通过libgpiod直接跟内核GPIO子系统打交道,API层次更低,行为更可预测。
还有一点很重要:libgpiod不需要像Jetson.GPIO那样做udev权限配置,它的工具和库直接通过/dev/gpiochip*访问,你只要把用户加到dialout组或者直接用sudo,就能跑起来。Python那边如果组权限没配好,折腾一圈,C++这边反而简单。
4.2 用命令行工具先玩一遍:gpioset和gpioget
libgpiod自带的命令行工具非常好用,很适合先做验证。比如还是控制物理引脚29,先用gpioinfo找到它在哪个chip的哪一行:
sudo gpioinfo gpiochip0假设发现它在gpiochip0的line 6,那么点灯就是:
# 指定输出模式并拉高 sudo gpioset gpiochip0 6=1 # 读回状态 sudo gpioget gpiochip0 64.3 C++ API示例:请求GPIO并控制电平
命令行验证通过之后,再写正式的C++代码就心里有底了。下面这个示例演示如何用libgpiod的C API打开GPIO芯片、请求一个行作为输出、设置电平:
#include <gpiod.h> #include <cstdio> #include <cstring> #include <unistd.h> int main() { const char* chipname = "gpiochip0"; unsigned int line_num = 6; // 根据你的gpioinfo输出修改 struct gpiod_chip* chip = gpiod_chip_open_by_name(chipname); if (!chip) { perror("Open chip failed"); return 1; } struct gpiod_line* line = gpiod_chip_get_line(chip, line_num); if (!line) { perror("Get line failed"); gpiod_chip_close(chip); return 1; } // 请求输出,初始值设为低电平 if (gpiod_line_request_output(line, "gpio-test", 0) < 0) { perror("Request line as output failed"); gpiod_chip_close(chip); return 1; } for (int i = 0; i < 10; ++i) { gpiod_line_set_value(line, 1); usleep(500000); gpiod_line_set_value(line, 0); usleep(500000); } gpiod_line_release(line); gpiod_chip_close(chip); return 0; }编译命令很简单:
g++ -o gpio_flash gpio_flash.cpp -lgpiod sudo ./gpio_flash如果你用的是C++而不是C,也可以直接用C++的封装库,但底层就是这一套,核心是明白gpiod_chip_open_by_name、gpiod_chip_get_line、gpiod_line_request_output这条调用链。
4.4 为什么"读输入"比"写输出"更容易翻车
读输入时有一个特别经典的坑:没有正确配置上拉/下拉,导致引脚悬空,读到的电平随机跳变。libgpiod的C API里,请求输入时可以指定内部电阻配置:
struct gpiod_line_request_config config; memset(&config, 0, sizeof(config)); config.consumer = "gpio-input"; config.request_type = GPIOD_LINE_REQUEST_DIRECTION_INPUT; config.flags = GPIOD_LINE_REQUEST_FLAG_BIAS_PULL_UP; // 启用内部上拉 if (gpiod_line_request(line, &config, 0) < 0) { perror("Request input with pull-up failed"); }Python里一行pull_up_down=GPIO.PUD_UP就能解决的事,在C++里要手动填结构体。这就是为什么我反复建议大家先摸清libgpiod的数据结构和标志位,别直接抄网上的代码,因为很容易漏掉这些细节。
5. Pinmux深度复用:把普通GPIO变成UART/SPI/PWM,不需要重刷系统
5.1 设备树Overlay:运行时切换引脚复用状态的正确姿势
现在到了标题里的核心——Pinmux引脚复用。如果你想用40Pin头上的某个引脚当UART或者SPI用,光改Python代码是没用的,必须让内核知道"这个引脚现在被分配给某个功能外设了"。
Orin Nano上的做法是设备树覆盖(Device Tree Overlay)。官方config-by-pin.py的本质,就是根据你交互式选择的引脚功能,生成一份对应的Overlay DTB,然后在Bootloader的extlinux配置里加载它。
我自己试过两条路线:
路线一:用官方工具生成
sudo /opt/nvidia/jetson-io/config-by-pin.py -c gpio它会提示你选择引脚和功能,然后自动生成一个类似p3449-0000-pinmux-overlay.dtbo的文件,放到/boot目录,最后在/boot/extlinux/extlinux.conf的FDT字段加上覆盖文件名。
路线二:手动写Overlay DTS,适合批量定制
官方工具适合单个引脚切换,但如果你要一次性把十几个引脚改成特定功能,手动写DTS更高效。
5.2 手写一个UART的Overlay DTS示例
以把40Pin头上第8脚(物理引脚8)和第10脚(物理引脚10)复用为UART1的TX/RX为例,思路是:
- 找到这两个物理引脚对应的SoC pinmux节点(在芯片手册里查,或者参考官方默认DTS里的映射表)。
- 在Overlay里修改
pinmux节点,把这两个引脚的PINMUX属性改成UART功能。 - 启用对应的UART控制器节点。
一个非常简化的DTS片段如下:
/dts-v1/; /plugin/; / { overlay-name = "Enable UART1 on Pins 8/10"; compatible = "nvidia,p3449-0000-b00", "nvidia,jetson-orin-nano"; fragment@0 { target = <&pinmux>; __overlay__ { pinctrl-0 = <&pinmux_uart1_pins>; pinmux_uart1_pins: uart1-pins { uart1_tx { nvidia,pins = "uart1_tx_pd3"; nvidia,function = "uart"; }; uart1_rx { nvidia,pins = "uart1_rx_pd2"; nvidia,function = "uart"; }; }; }; }; fragment@1 { target = <&uart1>; __overlay__ { status = "okay"; }; }; };然后编译并加载:
dtc -I dts -O dtb -o uart1-overlay.dtbo uart1-overlay.dts sudo cp uart1-overlay.dtbo /boot/修改/boot/extlinux/extlinux.conf,在FDT项后面追加uarta1-overlay.dtbo,重启即可。
这里必须提醒一句:这些引脚名字不是我瞎编的,但不同板卡的具体pinmux节点名可能不一样。你在自己板子上操作时,一定要先打开官方BSP包里的原始DTSI文件搜索确认。直接抄我这段代码,很可能重启后引脚没反应。
5.3 第三种方式:直接改内核源码的DTSI(不推荐但值得懂)
还有一种土办法是直接修改内核源码里的tegra234-p3737-0000-p3509-a02.dtsi这类文件,把引脚复用配置改掉,然后重新编译内核/设备树。这种方式适合硬件定制已经定型、要批量烧录的场景。
但日常开发我不推荐,原因很简单:改内核DTSI一旦出错,系统可能直接启动黑屏,而Overlay方式出错了还可以改extlinux配置回退,风险小得多。我见过有人图省事直接改DTSI,结果一个引脚配置写错,整块板子起不来,最后只能重新刷机。
5.4 Overlay生效后,怎么验证确实成功了
改完重启之后,别急着跑应用。先用系统工具验证一遍:
# 查看40Pin头当前功能状态 sudo /opt/nvidia/jetson-io/config-by-pin.py -l # 查看UART设备节点是否存在 ls /dev/ttyTHS* # 如果是SPI,查看SPI设备 ls /dev/spidev*只有这些节点出现了,才说明Pinmux配置生效了,接下来才轮到你的Python/C++代码上场。/dev/ttyTHS1这类设备节点,就是UART成功复用之后内核暴露给用户空间的接口。
6. 性能实测与选型建议:Python和C++在真实场景里的差距
6.1 GPIO切换频率实测
我自己在Orin Nano上做过一轮简单测试,比较Python(Jetson.GPIO)和C++(libgpiod)连续翻转一个GPIO引脚的极限频率。测试方法是写一个死循环翻转电平,用示波器看实际波形。
结果大致如下(不同系统负载下会有波动,但量级就是这样):
| 方法 | 实测最高翻转频率 | 特点 |
|---|---|---|
| Python + Jetson.GPIO | 几十kHz量级 | 代码简单,适合低速控制 |
| C++ + libgpiod | 几百kHz量级 | 延迟低,适合高频波形输出 |
| C++ + 直接操作寄存器 | MHz量级(需要root+mmap) | 不推荐普通应用做,太容易搞挂系统 |
这个差距的根因在于:Python的库调用链长,从Python解释器到cffi再到内核字符设备,每一层都有开销;而libgpiod的C API本身就很薄,直接ioctl到内核。
6.2 输入事件响应延迟,哪个更适合做中断触发
如果你要用GPIO检测外部信号,比如编码器脉冲、传感器触发,那么事件响应延迟比输出频率更重要。
我的测试方法:用信号发生器产生一个上升沿,分别在Python和C++里注册GPIO_FALLING_EDGE中断回调,用clock_gettime记录从内核报告事件到用户空间回调开始执行的时间差。
- C++ / libgpiod:实测在几十微秒到一两百微秒之间,因为可以设置实时线程优先级,效果更稳定。
- Python / Jetson.GPIO:通常在几百微秒到毫秒级,而且受系统调度影响较大,偶尔会出现几毫秒的抖动。
所以如果你的项目对时序有硬要求——比如做PPM信号解码、做高速计数——老老实实用C++。如果只是扫个按钮、读个温湿度传感器的电平信号,Python完全够用,别过度设计。
6.3 PWM和硬件的"坑":不要在Orin Nano上指望软件PWM
说到这里顺带提醒一句:Orin Nano的40Pin头虽然有PWM能力,但硬件PWM通道非常有限,通常只有一两个。很多人想用软PWM(比如Python里控制GPIO做占空比)驱动舵机或者调光灯,结果发现频率一高就变形。
根本原因是Linux不是实时系统,用户态软件PWM会受到调度器影响,波形抖动明显。更靠谱的方案有两类:
- 用硬件PWM外设,通过设备树Overlay把对应引脚复用为PWM功能,然后操作
/sys/class/pwm/pwmchip*。 - 外接一个PCA9685这类PWM驱动芯片,走I2C控制,16路PWM随便玩。
我之前在一个项目里用软PWM驱动MG996R舵机,电压倒是没出问题,但舵机抖得不行,最后换了PCA9685才算消停。这个教训分享给大家,不要在错误的层次上浪费时间。
7. 我踩过的坑,按严重程度排序给你列出来
7.1 坑一:黑屏危机——Overlay配置错误导致系统起不来
这是我踩得最狠的一个坑。当时为了让40Pin头的一个引脚切换成SPI功能,我改了一个自认为正确的DTSI文件,结果重启之后Orin Nano直接黑屏,串口一点输出都没有。
排查过程:Orin Nano的启动日志会先经过Bootloader再到内核,如果设备树解析失败,内核可能直接panic。我当时没有接串口调试线,只能盲操作,最后靠SD卡备份恢复才救回来。
经验总结:
- 改任何Pinmux配置前,先备份当前正在使用的DTB文件。
- 用
config-by-pin.py生成的Overlay永远比手写更安全,因为它是官方校验过的。 - 用
extlinux.conf加Overlay时,如果系统起不来,可以在Bootloader菜单里手动进入编辑模式,删掉FDT后面的Overlay字段,让系统用默认DTB启动。
7.2 坑二:5V引脚当GPIO用,差点烧了传感器
40Pin头上有些引脚是5V电源或5V逻辑电平,如果你把这些引脚接到3.3V的传感器上,后果可能就是烧模块。我之前就图省事,把一个5V引脚直接接了个3.3V的逻辑电平传感器,结果传感器冒烟了。
经验总结:
- Orin Nano的GPIO大部分是3.3V逻辑电平,绝对不能用5V电平直接驱动。
- 40Pin头上的5V引脚是电源输出,不是GPIO,别拿它当控制脚。
- 接任何外部模块之前,先去查官方引脚图,再三确认电平匹配,宁可多买几个电平转换模块,也不要烧硬件。
7.3 坑三:GPIO号老是查不准,加个动态打印帮你排雷
这个问题很隐蔽但很普遍:内核版本升级之后,gpiochip编号可能会变化,你的代码里如果写死了gpiochip0,下一次启动可能就失效了。
我的土办法是在程序启动时动态探测:
#include <filesystem> #include <fstream> #include <iostream> #include <string> std::string find_gpiochip_by_label(const std::string& label_prefix) { for (const auto& entry : std::filesystem::directory_iterator("/sys/class/gpio")) { std::string name = entry.path().filename().string(); if (name.rfind("gpiochip", 0) == 0) { std::ifstream label_file(entry.path().string() + "/label"); std::string label; std::getline(label_file, label); if (label.rfind(label_prefix, 0) == 0) { return "/dev/" + name; } } } return ""; }Python里也有类似思路,用glob扫一遍/sys/class/gpio拿到chip名再拼路径,比写死要稳得多。
7.4 坑四:Jetson.GPIO的"引脚编号"是板级还是芯片级,千万别混
这个是新手最容易犯的错误。Jetson.GPIO的GPIO.BOARD模式用的是40Pin物理引脚号,但很多网上的旧教程(特别是树莓派迁移过来的)会写GPIO.setmode(GPIO.BCM),在Orin Nano上BCM模式对应的是芯片级编号,也就是gpiochip里的行号,跟物理引脚完全不对应。
如果你发现代码里用了GPIO.BCM而且引脚写的是13、15、18这些数字,那很可能就是踩了这个坑。解决方式很简单:统一用GPIO.BOARD,以物理引脚为基准写代码。
7.5 坑五:事件回调里的"延时操作"导致中断丢失
用Python的GPIO.add_event_detect注册按键中断时,很多人会在回调函数里做time.sleep或者复杂的打印操作。这会导致一个问题:回调还没返回,新的边沿事件已经来了,内核的事件队列堆在那里,漏事件、卡事件都会出现。
正确做法是:回调函数里只做一个标志位或把事件塞进队列,立即返回;耗时的处理放到主循环或者另一个线程里去做。
import threading import queue import Jetson.GPIO as GPIO event_queue = queue.Queue() def button_callback(channel): event_queue.put(channel) GPIO.setmode(GPIO.BOARD) GPIO.setup(31, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.add_event_detect(31, GPIO.FALLING, callback=button_callback, bouncetime=200) def worker(): while True: ch = event_queue.get() print(f"Button event on channel {ch}") # 这里再处理耗时的逻辑 threading.Thread(target=worker, daemon=True).start()这个模式无论Python还是C++都成立:中断回调里只记录,不处理。我在C++的libgpiod里也一样,回调里把timespec和event_type存进环形缓冲区,业务逻辑另外起线程消费,实测丢事件率极低。
8. 写在最后:我的切身经验与后续扩展建议
折腾Orin Nano的GPIO,真正让我觉得有门槛的不是写代码,而是理解硬件和系统之间的配合逻辑。树莓派把所有东西都封装好了,你做应用开发就行;Orin Nano更像一块嵌入式主板,你得先跟内核、设备树、Pinmux这些底层概念打好交道,才能让上层代码跑得顺。
我的建议很简单:
- 第一次上手,直接用官方JetPack系统,别先折腾第三方内核和定制DTB。
- 先把
config-by-pin.py -l的输出截图存下来,这就是你的"引脚地图"。 - 点灯用Python,正式项目用C++,别在软PWM上浪费时间。
- 接线之前查电平,改设备树之前备份,这是两条救命原则。
后面我还在计划把/dev/ttyTHS串口和GPIO结合起来做一套简单的MODBUS RTU从站,顺便试一下Orin Nano在实时控制场景下的极限。如果你也在折腾这块板子,欢迎一起交流踩坑经验。