Orin Nano GPIO remap without Jetson-IO or Pinmux changes (headless minimal image)

Hi All,

I’ve been using the Jetson Nano for some time with a custom header board I designed and had manufactured. It connects directly to the 40-pin header and controls button press + LEDs using the original Nano GPIO layout—no soldering required.

I’m now migrating to the Jetson Orin Super Nano (r36, JetPack 6.x) and hitting GPIO layout differences. My system is headless and uses a minimal custom image, so I cannot run jetson-io.py interactively, and is designed to burn an SD card, and load and start working right away.

I don’t want to redesign my hardware. My code already detects the platform at runtime, so I’m fine remapping to different logical pins on the Orin—as long as I can do it without modifying the Pinmux spreadsheet or using jetson-io.py.

Question:

Can I simply target different GPIO numbers (via libgpiod / Sysfs) on Orin without changing the default Pinmux configuration?

I tried mapping to what I thought were equivalent logical pins (based on the Orin 40-pin header docs), but as soon as I initialize the GPIO controller (using LibGpioDriver in C# / ARM64), the entire board powers off.

What I’ve tried:

  • Old setup (Nano, r32): SysFsDriver → worked perfectly

  • New setup (Orin, r36): Switched to LibGpioDriver (required for r36+)

  • Attempted GPIO calls on presumed pins → instant power-off (Red, 144)


// JETSON NANO — default sysfs numbers  (local numbers using SysFsDriver() in netcore)
_button1PinNumber = 216;
_redPinNumber     = 79;
_greenPinNumber   = 50;
_bluePinNumber    = 14;

Additional context:

Has anyone successfully remapped GPIOs on Orin without modifying the Pinmux?
The pinmux spreadsheet is intimidating, and as a hobbyist (not a hardware engineer), I’m lost trying to interpret it. I just want to reuse my existing Nano header board to control a few LEDs and buttons — without redesigning and remanufacturing hardware. Any guidance, even small tips, would be greatly appreciated!

Possible Mappings?

_button1PinNumber = 144;
_redPinNumber     = 50;
_greenPinNumber   = 112;
_bluePinNumber    = 122;

Any help is greatly appreciated — thanks in advance!

Hi travis3,

Although jetson-io.py can be used through serial console, I would not suggest using it as you are using the custom carrier board rather than the devkit.
Using pinmux spreadsheet is required and necessary to configure the pin.
We are not clear about your custom design, you have to use pinmux spreadsheet to configure the usage for each pins according to your design and requirement.

Sorry that I’m not quite understand about your question here.
Do you just want to use PIN7/PIN11/PIN12/PIN13 of J12 as GPIO/Output for your Button/LED function?
Please note that the sysfs method to control GPIO has been deprecated in JP6.x.

Hi — to clarify:

I’m still using the Jetson Orin Nano DevKit with a minimal Ubuntu image built via pythops/jetson-image. This lets me include only the packages I need.

jetson-io.py is present but fails immediately on launch (exits without output). Even if it worked, I’d prefer to avoid interactive tools — I want to fully script GPIO configuration at boot so my headless app starts without any user input.

I don’t understand the pinmux spreadsheet or SoC-level config. I’m not a hardware/SoC expert — just trying to reuse my Nano header board.
Is there a simple command-line way (e.g. config file + script) to set specific 40-pin header pins as GPIOs before my app runs?

Any pointers toward a non-interactive, scriptable GPIO setup would be hugely appreciated!

It is not the expected result to me if you are using the devkit.

Pinmux spreadsheet is loaded in early boot stage(i.e. MB1) to configure the pin state.

Is config-by-function.py something you are looking for?
If so, please refer to Configuring the Jetson Expansion Headers — config-by-function: Configure Header(s) by Special Function for details.

You can also try manually updating the kernel device tree to configure the pinmux, which will be loaded by pinctrl driver. Please just confirm you’ve configured them correctly.

I was trying that – but get this:

root@svr:~#  /opt/nvidia/jetson-io/config-by-function.py -l all
Traceback (most recent call last):
  File "/opt/nvidia/jetson-io/config-by-function.py", line 184, in <module>
    main()
  File "/opt/nvidia/jetson-io/config-by-function.py", line 133, in main
    jetson = board.Board()
             ^^^^^^^^^^^^^
  File "/opt/nvidia/jetson-io/Jetson/board.py", line 234, in __init__
    self.appdir = _board_partition_mount(mountpart)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/nvidia/jetson-io/Jetson/board.py", line 159, in _board_partition_mount
    raise RuntimeError("No %s partition found!" % partlabel)
RuntimeError: No APP partition found!
root@svr:~# /opt/nvidia/jetson-io/config-by-pin.py -p 7
Traceback (most recent call last):
  File "/opt/nvidia/jetson-io/config-by-pin.py", line 84, in <module>
    main()
  File "/opt/nvidia/jetson-io/config-by-pin.py", line 39, in main
    jetson = board.Board()
             ^^^^^^^^^^^^^
  File "/opt/nvidia/jetson-io/Jetson/board.py", line 234, in __init__
    self.appdir = _board_partition_mount(mountpart)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/nvidia/jetson-io/Jetson/board.py", line 159, in _board_partition_mount
    raise RuntimeError("No %s partition found!" % partlabel)
RuntimeError: No APP partition found!

This is R36.4.4 base from jetson-image/Containerfile.image.l4t36 at master · pythops/jetson-image · GitHub

Could you share the result of the following commands on your board?

$ ls -al /dev/disk/by-partlabel
$ cat /etc/nv_boot_control.conf
root@svr:~# ls -al /dev/disk/by-partlabel
total 0
drwxr-xr-x 2 root root 340 Jan  1  1970 .
drwxr-xr-x 8 root root 160 Jan  1  1970 ..
lrwxrwxrwx 1 root root  15 Jan  1  1970 A_kernel -> ../../mmcblk0p2
lrwxrwxrwx 1 root root  15 Jan  1  1970 A_kernel-dtb -> ../../mmcblk0p3
lrwxrwxrwx 1 root root  15 Jan  1  1970 APP -> ../../mmcblk0p1
lrwxrwxrwx 1 root root  15 Jan  1  1970 A_reserved_on_user -> ../../mmcblk0p4
lrwxrwxrwx 1 root root  15 Jan  1  1970 B_kernel -> ../../mmcblk0p5
lrwxrwxrwx 1 root root  15 Jan  1  1970 B_kernel-dtb -> ../../mmcblk0p6
lrwxrwxrwx 1 root root  15 Jan  1  1970 B_reserved_on_user -> ../../mmcblk0p7
lrwxrwxrwx 1 root root  16 Jan  1  1970 esp -> ../../mmcblk0p10
lrwxrwxrwx 1 root root  16 Jan  1  1970 esp_alt -> ../../mmcblk0p13
lrwxrwxrwx 1 root root  15 Jan  1  1970 recovery -> ../../mmcblk0p8
lrwxrwxrwx 1 root root  16 Jan  1  1970 recovery_alt -> ../../mmcblk0p11
lrwxrwxrwx 1 root root  15 Jan  1  1970 recovery-dtb -> ../../mmcblk0p9
lrwxrwxrwx 1 root root  16 Jan  1  1970 recovery-dtb_alt -> ../../mmcblk0p12
lrwxrwxrwx 1 root root  16 Jan  1  1970 reserved -> ../../mmcblk0p15
lrwxrwxrwx 1 root root  16 Jan  1  1970 UDA -> ../../mmcblk0p14
root@svr:~# cat /etc/nv_boot_control.conf
TNSPEC 3767-300-0005-T.1-1-0-jetson-orin-nano-devkit-
COMPATIBLE_SPEC 3767--0005--1--jetson-orin-nano-devkit-
TEGRA_BOOT_STORAGE mmcblk0
TEGRA_CHIPID 0x23
TEGRA_OTA_BOOT_DEVICE /dev/mtdblock0
TEGRA_OTA_GPT_DEVICE /dev/mtdblock0

On JetPack 6.x minimal or headless image (root-on /dev/mmcblk0p1, no A/B slots), the official jetson-io tools will fail:

$ sudo /opt/nvidia/jetson-io/config-by-pin.py 
RuntimeError: No APP partition found!

Same error with config-by-function.py and jetson-io.py.

Exact cause (verified in the actual source)

File: /opt/nvidia/jetson-io/Jetson/board.py

activepart = syscall.call_out('nvbootctrl -t rootfs get-current-slot')
if activepart and activepart[0] == '0':
    mountpart = "APP"
elif activepart and activepart[0] == '1':
    mountpart = "APP_b"
else:
    raise RuntimeError("Failed to get active rootfs partition!")

# ... later
self.appdir = _board_partition_mount(mountpart)   # line ~234

On minimal/headless images:

  • nvbootctrl either doesn’t exist or returns nothing → activepart is empty/None

  • The if/elif blocks never run → mountpart is never assigned

  • The code continues anyway and tries to mount the partition labeled “APP”

  • No such partition exists → “No APP partition found!”

Even when the else: correctly raises an exception, some exception-handling paths still reference the undefined mountpart variable, which triggers the same error.

Minimal fix that makes the tools run again

Just add a safe fallback when nvbootctrl doesn’t return 0 or 1:

- activepart = syscall.call_out('nvbootctrl -t rootfs get-current-slot')
- if activepart and activepart[0] == '0':
+ activepart = syscall.call_out('nvbootctrl -t rootfs get-current-slot').strip()
+ if activepart == '0':
      mountpart = "APP"
- elif activepart and activepart[0] == '1':
+ elif activepart == '1':
      mountpart = "APP_b"
  else:
-    raise RuntimeError("Failed to get active rootfs partition!")
+    # Non A/B system – fall back to using /boot directly
+    mountpart = None

and in _board_partition_mount() / the _init_ add:

if mountpart is None:
    return "/boot"   # overlays live directly in /boot on minimal images

After this change the tools start normally, show the menu, and can generate and install .dtbo files again.

Note - I have no idea if this is even an acceptable practice.

Request to NVIDIA

The jetson-io suite currently assumes every image uses L4T’s A/B update mechanism with APP/APP_b partitions.

In our deployment, we use the minimal image without A/B slots, so the tool is broken.

If the patch above is acceptable (not for me to say), please add the simple fallback above (or equivalent) so the tools work out-of-the-box on all official JetPack images.

Now that I can get the tool to work. I just need to understand how to set those pins up correctly. The Excel spreadsheet I am not understanding. If you can tell me what lines to edit, I can run the generate from there.

Thanks!

I’m trying to configure 4 pins on the 40-pin header as GPIO on a minimal/headless image:

  • Pin 7: GPIO input, active-low (button)

  • Pins 11, 12, 13: GPIO outputs, default off (LEDs)

Issues encountered:

  1. Model/DTB mismatch: The running system reports model as "NVIDIA Jetson Orin Nano Developer Kit Super" but the DTB files in dtb are named for the non-Super variant. Specifically missing: /boot/dtb/kernel_tegra234-p3768-0000+p3767-0005-super.dtb

    The only DTB present is: kernel_tegra234-p3768-0000+p3767-0005-nv.dtb

  2. jetson-io tools fail: config-by-pin.py and config-by-function.py error out because the minimal image lacks APP/APP_b partitions and has the DTB naming mismatch mentioned above.

  3. Pinmux spreadsheet confusion: As a hobbyist (not a hardware engineer), I’m struggling to interpret the pinmux spreadsheet to generate the correct device tree overlay. If you can screen shot to me what needs to change, I can try it myself, but don’t know what to select here for these pins.

What I need:

Could someone provide:

  • A pre-built .dtbo file that configures pins 7, 11, 12, 13 as GPIO (with pin 7 as active-low input)?

  • Instructions on where to place it and how to load it at boot?

I’m hoping for a simple overlay solution that remaps just these 4 pins without requiring a full pinmux rebuild or complex jetson-io configuration.

Thank you!

I successfully configured custom GPIO pins on my Jetson Orin Nano Super using device tree overlays and jetson-io.py. Here’s what worked for me, which may help others in similar situations:

What I Needed

  • Pin 7: Button input (active-low with pull-up)

  • Pins 11, 12, 13: GPIO outputs for RGB LED control

Resources That Helped

  1. JetsonHacks GPIO Patch Repository: https://github.com/jetsonhacks/jetson-orin-gpio-patch

    • Excellent reference for pin mappings in headers/tegra234-p3767-0000-common-hdr40.dtsi (didn’t even look at it, CoPilot did)

    • Example DTS structure from pin7_as_gpio.dts

    • Associated YouTube video provides great background on the approach

    • Note: I didn’t need the kernel patch itself, just used it as reference documentation

  2. AI-Assisted DTS Generation: Used AI (GitHub Copilot) to generate the custom DTS file based on my pin requirements and the reference files from the repo above.

Critical Issue with Custom/Minimal Images

The jetson-io.py tool needed patching to work with custom-built images!

The stock jetson-io.py script has hardcoded DTB filename patterns that don’t match what is available in the 36.4.4 base. Specifically:

  • It expects files like kernel_tegra234-p3768-0000+p3767-0005-nv-super.dtb when what was available was kernel_tegra234-p3768-0000+p3767-0005-nv.dtb

  • The script also didn’t handle the -super suffix in board names properly (though it falls back gracefully)

Jetson-IO Patch Required:

diff --git a/Jetson/board.py b/Jetson/board.py
index 5308b2d..74d5f12 100644
--- a/Jetson/board.py
+++ b/Jetson/board.py
@@ -147,6 +147,18 @@ def _board_partition_is_root_mountpoint(partlabel):
 def _board_get_dtb(compat, model, path):
     dtbs = dtc.find_compatible_dtb_files(compat, model, path)
 
+    # If exact model match fails, try finding DTB by compatible string only
+    # This handles cases like "Engineering Reference" vs regular boards
+    if dtbs is None:
+        # Try matching just by compatible string
+        import glob
+        pattern = os.path.join(path, "kernel*.dtb")
+        for dtb_file in glob.glob(pattern):
+            dtb_compat = dtc.get_compatible(dtb_file)
+            if dtb_compat and any(c in compat for c in dtb_compat.split()):
+                dtbs = [dtb_file]
+                break
+    
     if dtbs is None:
         raise RuntimeError("No DTB found for %s!" % model)
     if len(dtbs) > 1:
@@ -216,12 +228,18 @@ class Board(object):
 
         #Finding the active partition in case of redundant rootfs flash.
         activepart = syscall.call_out('nvbootctrl -t rootfs get-current-slot')
-        if activepart[0] == '0':
+        if activepart and activepart[0]:
+            activepart = activepart[0].strip()
+        else:
+            activepart = None
+
+        if activepart == '0':
             mountpart = "APP"
-        elif activepart[0] == '1':
+        elif activepart == '1':
             mountpart = "APP_b"
         else:
-            raise RuntimeError("Failed to get active rootfs partition!")
+            # Non A/B system – fall back to using /boot directly
+            mountpart = None
 
         # When mounting the rootfs via NFS, the root partition is not a
         # block device. Furthermore, when booting with NFS the partition
@@ -230,7 +248,7 @@ class Board(object):
         # not mounted with the active partition, then it is necessary to
         # find and mount the active partition and copy the generated
         # files back to this partition.
-        if not _board_partition_is_root_mountpoint(mountpart):
+        if mountpart is not None and not _board_partition_is_root_mountpoint(mountpart):
             self.appdir = _board_partition_mount(mountpart)
             dtbdir = os.path.join(self.appdir, 'boot/dtb')
             fio.is_rw(self.appdir)

Steps That Worked

1. Create Custom DTS File

/dts-v1/;
/plugin/;

/ {
    jetson-header-name = "Jetson 40pin Header";
    overlay-name = "My Custom Header 1.0";
    compatible = "nvidia,p3768-0000+p3767-0000\0nvidia,p3768-0000+p3767-0001\0nvidia,p3768-0000+p3767-0003\0nvidia,p3768-0000+p3767-0004\0nvidia,p3768-0000+p3767-0005\0nvidia,p3768-0000+p3767-0000-super\0nvidia,p3768-0000+p3767-0001-super\0nvidia,p3768-0000+p3767-0003-super\0nvidia,p3768-0000+p3767-0004-super\0nvidia,p3768-0000+p3767-0005-super\0nvidia,p3509-0000+p3767-0000\0nvidia,p3509-0000+p3767-0001\0nvidia,p3509-0000+p3767-0003\0nvidia,p3509-0000+p3767-0004\0nvidia,p3509-0000+p3767-0005";

    fragment@0 {
        target = <&pinmux>;
        __overlay__ {
            pinctrl-names = "default";
            pinctrl-0 = <&jetson_io_pinmux>;
            
            jetson_io_pinmux: exp-header-pinmux {
                hdr40-pin7 {
                    nvidia,pins = "soc_gpio59_pac6";
                    nvidia,tristate = <0x1>;        /* Enable for input */
                    nvidia,enable-input = <0x1>;    /* Enable input receiver */
                    nvidia,pull = <0x2>;            /* Pull-up resistor */
                };

                hdr40-pin11 {
                    nvidia,pins = "uart1_rts_pr4";
                    nvidia,tristate = <0x0>;        /* Enable output driver */
                    nvidia,enable-input = <0x1>;    /* Bidirectional capability */
                    nvidia,pull = <0x0>;            /* No pull resistor */
                };

                hdr40-pin12 {
                    nvidia,pins = "soc_gpio41_ph7";
                    nvidia,tristate = <0x0>;
                    nvidia,enable-input = <0x1>;
                    nvidia,pull = <0x0>;
                };

                hdr40-pin13 {
                    nvidia,pins = "spi3_sck_py0";
                    nvidia,tristate = <0x0>;
                    nvidia,enable-input = <0x1>;
                    nvidia,pull = <0x0>;
                };
            };
        };
    };
};

2. Compile and Install

# Compile DTS to DTBO
dtc -O dtb -o my_custom_header_1.0.dtbo my_custom_header_1.0.dts

# Copy to boot directory
sudo cp my_custom_header_1.0.dtbo /boot/

# Apply patch to jetson-io.py (if jetson-io fails to run)
[Apply the patch above to /opt/nvidia/jetson-io/jetson-io.py]

# Run jetson-io
sudo /opt/nvidia/jetson-io/jetson-io.py
# Select your custom overlay from the menu

# Reboot
sudo reboot

3. Verify

# Check that overlay is loaded
cat /boot/extlinux/extlinux.conf
# Should show: OVERLAYS /boot/my_custom_header_1.0.dtbo

# Test GPIO using libgpiod tools
gpioinfo gpiochip0 | grep -E "line\s+(50|112|122|144)"

Testing GPIO

GPIO Line Mappings:

  • Pin 7 (button) → line 144 (PAC.06)

  • Pin 11 → line 112 (PR.04)

  • Pin 12 → line 50 (PH.07)

  • Pin 13 → line 122 (PY.00)

Test Commands:

# Read button state
gpioget gpiochip0 144

# Set output high
gpioset -m exit gpiochip0 50=1

# Monitor button events
gpiomon --falling-edge gpiochip0 144

Key Learnings

  1. Excel sheet not required: While NVIDIA provides pinmux spreadsheets, the header files in the jetsonhacks repo were sufficient for finding pin names

  2. Jetson-IO works great once patched: Helpful to patch the system

  3. Button debouncing: Mechanical buttons will bounce - handle debouncing in software (50-100ms filtering) - Not sure if I can add something in the dts file which would do this for me - that would be sweet

Working Result

Successfully controlling RGB LEDs via GPIO and reading button presses using standard libgpiod tools (gpioset, gpioget, gpiomon).

Hope this helps anyone else working with custom images or trying to configure GPIO on the Orin platform / DevKit / minimal images.

Good to hear that you’ve managed this issue and share the experienece.

I just want to mention the super mode, which is supported in JP6.2.1(r36.4.4).

From the result you shared, you didn’t flash it with super mode board config so that you don’t have /boot/dtb/kernel_tegra234-p3768-0000+p3767-0005-super.dtb
If you want to enable super mode, please simply use jetson-orin-nano-devkit-super.conf as board config to flash the board.

Thanks for the hint - I was able to rebuild with the -super and all is working well. Even the overlay worked without having to reconfigure. I did a PR to the GitHub project for the super support with supporting docs for someone else who might run into it. Thanks again for the help. #150 - feat: add support for Jetson Orin Nano Super board by TWhidden · Pull Request #165 · pythops/jetson-image · GitHub