Jetson Customize Pinmux
NVIDIAOfficialCreated by Jetson TeamDistributed by NVIDIA
- Skill
- Tool integration
- Cloud and DevOps
- Apache-2.0
- Medium risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The jetson-customize-pinmux skill provides a structured workflow for configuring per-pin SFIO, direction, and initial state for Jetson Orin or Thor custom carriers. It parses the official Tegra pinmux spreadsheet (XLSM) to generate the necessary BCT DTSI files (pinmux, gpio, and padvoltage). The skill ensures that pin configurations are accurately captured, validated, and integrated into the carrier's bootloader configuration, following a strict interactive process to prevent hardware misconfiguration.
What it does
- Parses Tegra pinmux XLSM files to establish a ground truth for SoC pin configurations.
- Provides an interactive lookup tool to identify pins by ball name, signal, or DT name.
- Guides users through a mandatory Q1-Q6 interactive loop to set pin attributes.
- Generates pinmux, gpio, and padvoltage BCT DTSI files for custom carrier boards.
- Updates carrier configuration references to include the newly generated DTSI files.
- Ensures configuration idempotency by using specific markers in the generated files.
When to use it
- When you need to repurpose a CVM ball (e.g., switching between GPIO and a peripheral function).
- After deriving a custom carrier and needing to apply specific pinmux settings from an XLSM.
- When a pin mismatch is reported by other Jetson customization skills.
- When you need to define initial pin states, drive types, or pull attributes for custom hardware.
Advantages
- Ensures consistency by using the official XLSM as the single source of truth.
- Prevents configuration errors through a mandatory, guided per-pin interactive process.
- Automates the complex generation of three distinct BCT DTSI files.
- Integrates cleanly with existing Jetson BSP workflows and overlay trackers.
Usage examples
- “Configure pin H10 on my custom carrier to act as a GPIO.”
- The agent will look up pin H10, confirm it is GPIO-capable, and prompt you through the required direction and initial state settings before generating the updated DTSI files.
- “I need to set the drive type for the PCIE4_RST_L signal.”
- The agent will identify the corresponding DT pin, check if the pad is configurable, and if so, guide you through the Q4-Q6 attributes including drive type.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/nvidia/jetson-customize-pinmux/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip jetson-customize-pinmux.zip -d .agents/skills
Documentation by Jetson Team
Customize pinmux (per-pin SFIO / direction / state)
Overview
The Tegra pinmux spreadsheet (.xlsm) is the ground truth for every
CVM ball: SoC pin name, supported SFIOs, customer-selected function,
direction, and initial state. This skill parses that XLSM, runs a
per-pin Q1–Q6 interactive loop, and emits the three BCT DTSIs
(pinmux, gpio, padvoltage) in one shot into the overlay tracker
at <source.root_path>/Linux_for_Tegra/bootloader/.
Unlike sibling skills jetson-customize-uphy / jetson-customize-pcie /
jetson-customize-camera, pinmux has no kernel-DT overlay surface and no
ODMDATA edit. The XLSM is the source of truth; the three emitted
DTSIs land at flash time via the carrier conf's
PINMUX_CONFIG= / GPIOINT_CONFIG= / PMC_CONFIG= references (which
/jetson-derive-carrier set up).
Pad classification (silicon-fixed): only BD* and BI* pads have
configurable pull / drive / open-drain attributes. LP5XA_*,
UPHYDS_*, DP_SINGLE_*, BDMIPI16X_*, BDUSB2_*, OSCI27_* are
fixed-function and skip Q4–Q6 (configurable: no).
The bundled scripts/modify_pinmux.py is the workhorse: it parses
the XLSM via openpyxl>=3.1, builds the per-carrier pinmap JSON,
captures pin edits into a session shim, and (on generate) writes
the three DTSIs.
When to invoke
The user says "configure pin", "set SFIO", "edit pinmux DTSI", "set pin direction", "set initial state", or asks to repurpose a CVM ball (e.g. flip a pin between GPIO and a peripheral function).
A sibling skill (
jetson-customize-camera,jetson-customize-pcie,jetson-customize-usb,jetson-customize-mgbe) reports an HSIO pin mismatch viapin_verifier.pyand the user wants to fix it.The user pre-derived a custom carrier with
/jetson-derive-carrierand now wants to author the pinmux from a freshly-edited.xlsm.
Prerequisites:
Active profile selected (
target-platform/active_target.yml→<profile>.yamlwithreference_devkit:ANDcustom_carrier:).<source.root_path>/Linux_for_Tegra/exists as a git repo (/jetson-init-source)./jetson-derive-carrierhas run — the three pinmux-side BCT DTSIs (PINMUX_CONFIG,GPIOINT_CONFIG,PMC_CONFIGreferences in the carrier conf) exist in the overlay tracker.A pinmux
.xlsmis registered in the active profile atdocuments.custom_carrier_pinmux_xls(preferred when custom-carrier- specific) ordocuments.ref_devkit_pinmux_xls(fallback). The bundledmodify_pinmux.pyrequiresopenpyxl>=3.1.
Procedure
See references/procedure.md for the full
step-by-step procedure (Steps 1–8). Summary:
Resolve active target + XLSM. Validate active profile,
custom_carrier:, overlay-tracker prerequisites; resolve the pinmux.xlsmpath fromdocuments.custom_carrier_pinmux_xls→documents.ref_devkit_pinmux_xls→ single XLSM underdocuments.root_path→ user prompt.Probe. Run
modify_pinmux.py probeto parse the XLSM into the per-skill scratch<KB>/pinmap/<custom-carrier>.jsonplussession.jsonshim.Lookup. Resolve a free-form user query (CVM ball, Verilog name, signal, DT pin) via
modify_pinmux.py lookup; surface supported SFIO list, defaults, andconfigurable: yes/no.Set-pin (HARD GATE — Q1–Q6 via
AskUserQuestion). Q1–Q3 (sfio/direction/initial_state) always asked; Q4–Q6 (pull/drive_type/open_drain) only whenconfigurable: yes.tristateande_inputare derived fromdirection, never asked.Generate.
modify_pinmux.py generate --out-dir <source.root_path>/Linux_for_Tegra/bootloader/(root, notbootloader/generic/BCT/— derive-carrier.dtsforks live there, do not colocate). Emits:tegra<soc>-mb1-bct-{pinmux,gpio,padvoltage}-<carrier-key>.dtsi.<carrier-key>comes from the carrier conf'sPINMUX_CONFIG=reference, NOT the kebab-cased carrier name.Commit (single batched commit per workflow rule). All three DTSIs are one logical edit → one customization commit. Run the commit-preview gate before each commit.
Run-state sidecar + session shim. Write the user-facing
<profile-stem>.jetson-customize-pinmux.jsonsidecar and the transientsession.jsonshim under<workspace>/target-platform/.Summary. Emit the standard one-line + table summary.
Gotchas
No kernel-DT overlay; no
OVERLAY_DTB_FILEedit; norender_conf.pyhand-off. This skill ends at the three BCT DTSIs — the carrier conf already references them viaPINMUX_CONFIG=/GPIOINT_CONFIG=/PMC_CONFIG=(set up by/jetson-derive-carrier).Re-point the
.dtswrapper's#includeaftergenerate./jetson-derive-carrierforks the.dtswrappers atbootloader/generic/BCT/, but their#includelines may still pull the upstream devkit.dtsi(e.g.…-p3834-xxxx-p4071-0000.dtsi). Aftergeneratewrites the new<CARRIER_KEY>.dtsitobootloader/root, edit each wrapper's#includeto the new filename — by bare basename (#include "tegra<soc>-mb1-bct-pinmux-<CARRIER_KEY>.dtsi"), not../../…filesystem-relative. The BCT build'scpp -I bootloader/resolves bare basenames; that's the convention every other BCT include in the tree follows. Roll the wrapper edits into the same customization commit as the three DTSIs. Seereferences/procedure.mdStep 5 ("Sanity-check the carrier.dtswrapper").Q4–Q6 gated on
configurable: yes. Asking pull / drive_type / open_drain on a fixed-function pad (LP5XA_*,UPHYDS_*,BDMIPI16X_*, etc.) is silently dropped by the script and confuses the user.lookupprintsconfigurable: yes/no— always check it before prompting Q4–Q6.tristateande_inputare derived, never asked.unused→ tristate=ENABLE;input/bidirectional→ enable-input=ENABLE. Exposing them as separate prompts produces inconsistent DTSIs.sfio=gpiorequires a parseablegpio=GPIOn_PD.NNentry in the pinmap row'ssfiolist. Pins without one are GPIO-incapable silicon;set-pinrejects the call. Surface the rejection — don't silently fall back to a non-GPIO SFIO.Marker idempotency. Every per-pin edit carries
// custom-bsp: pinmuxon the closing brace; gpio default-state entries carry the same marker as a trailing comment. Re-runninggeneratemust detect and update — never duplicate.modify_pinmux.pyis unchanged from the original framework — it reads its ownsession.jsonshim under--kb-dir. The shim is regenerated each run from the active profile + the user-facing sidecar. Do not hand-edit the shim; it's transient.Multiple pinmux DTSI variants per Thor module SKU. Some carrier pins live in a different DTSI variant than the one the carrier conf references.
modify_pinmux.py commit(legacy patch-in-place flow) tolerates missing per-pin blocks viapinmux.warnings[]rather than failing. Surface the warning; point at the alternate DTSI variant.Don't touch the upstream BSP at
<bsp_image.root_path>. All edits land in<source.root_path>/Linux_for_Tegra/bootloader/under the pristine + customization commit pattern.
Available Scripts
| Script | Purpose | Arguments |
|---|---|---|
scripts/modify_pinmux.py | XLSM parser + per-pin DTSI generator. Invoked via run_script() from Steps 3-6 with the subcommand of the current phase. | probe | lookup | set-pin | apply | generate | commit [...] (see --help) |
scripts/generate_dtsi.py | Renders pinmux/GPIO/padvoltage DTSI fragments from the bundled session state. Called by modify_pinmux.py generate. | --session <path> --out-dir <dir> |
Invoke from the skill body as a subprocess via run_script():
# run_script: probe the carrier pinmux XLSM and write a session state
scripts/modify_pinmux.py probe --xlsm carrier.xlsm --session .pinmux-session.json
# run_script: render DTSI fragments from the final session state
scripts/modify_pinmux.py generate --session .pinmux-session.json --out-dir bsp_sources/pinmux/References
references/procedure.md— full Step 1–8 procedure prose.questions.json— Q1–Q6 prompt schema consumed by Step 4.../../references/platform_template.yaml—documents:block (ref_devkit_pinmux_xls,custom_carrier_pinmux_xls).../../context/bsp-customization-workflow.md— overlay edit protocol (single-commit per DTSI fork).../jetson-derive-carrier/SKILL.md— must run first; produces the pinmux / gpio / padvoltage DTSI forks this skill edits, and rewrites the carrier conf'sPINMUX_CONFIG=/GPIOINT_CONFIG=/PMC_CONFIG=lines to point at them.../jetson-init-source/SKILL.md— produces the overlay tracker this skill commits into.../jetson-link-docs/SKILL.md— author the profile'sdocuments:block, including the pinmux XLSM bindings.
Files
- ATTRIBUTION.mdAdded by AutoGineer932 B
- BENCHMARK.md3.9 KiB
- LICENSELicence file added by AutoGineer11.2 KiB
- SKILL.md9.5 KiB
- evals/evals.json4.0 KiB
- questions.json2.5 KiB
- references/procedure.md16.6 KiB
- scripts/_pinmux_common.pyScript8.2 KiB
- scripts/_pinmux_dt.pyScript8.9 KiB
- scripts/generate_dtsi.pyScript32.8 KiB
- scripts/modify_pinmux.pyScript29.4 KiB
- skill-card.md3.5 KiB
- skill.oms.sig5.9 KiB
scripts/generate_dtsi.py
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Generate pinmux / gpio / padvoltage DTSIs from a Jetson Thor pinmux XLSM.
Replicates the "Customer Generate DT" Excel macro on Linux without requiring
Excel or LibreOffice. Reads the XLSM with openpyxl (no VBA execution) and
emits three DTSI files that are byte-for-byte compatible with the macro output.
Usage::
python3 generate_dtsi.py \\
--xlsm /path/to/Jetson_Thor_Series_Modules_Pinmux_Template_v1.7.xlsm \\
--out-dir /path/to/Linux_for_Tegra/bootloader \\
--carrier-name p3834-xxxx-p4071-0000 # suffix used in filenames
Output (all in --out-dir):
tegra264-mb1-bct-pinmux-<carrier-name>.dtsi
tegra264-mb1-bct-gpio-<carrier-name>.dtsi
tegra264-mb1-bct-padvoltage-<carrier-name>.dtsi
Verified against Thor r38.4 devkit DTSIs generated by
T264_Pinmux_Config_CVM_Jedha_P4070.xlsm Revision 9.
"""
from __future__ import annotations
import argparse
import copy
import pathlib
import re
import sys
from collections import defaultdict
from dataclasses import dataclass, field
try:
import openpyxl
except ImportError:
openpyxl = None
# ---------------------------------------------------------------------------
# Column map (1-based) — Thor v1.7 template, Revision 39
# Validated against row 7 headers; see _detect_columns().
# ---------------------------------------------------------------------------
THOR_COLS: dict[str, int] = {
"connector_pin": 1,
"signal_name": 2,
"package_ball": 3, # "MPIO" in header row
"verilog_name": 4,
"mux_unused": 5,
"mux_gpio": 6,
"mux_sfio0": 7,
"mux_sfio1": 8,
"mux_sfio2": 9,
"mux_sfio3": 10,
"dir_gpio": 11,
"dir_sfio0": 12,
"dir_sfio1": 13,
"dir_sfio2": 14,
"dir_sfio3": 15,
"dt_pin_name": 21,
"power_rail": 22,
"pad_category": 28,
"pad_type": 29,
"pull_strength": 30,
"por_state": 31,
"pin_group": 32,
"pupd": 34,
"tristate": 35,
"e_input": 36,
"gpio_init_val": 37, # AK — GPIO Init Value: z / 0 / 1
"customer_usage": 45,
"pin_direction": 46,
"e_io_od": 50, # AX — Open-Drain Select
"drv_type": 51, # AY — DRV_TYPE: Disable=1X, Enable=High(2X)
"e_lpbk": 52, # AZ — Loopback Select
"io_voltage": 61, # BI — IO Block Voltage e.g. "1.8V" / "3.3V"
}
_NON_DATA_SHEETS = {
"customer-readme", "quick guide", "revision history",
"revision history internal only", "template",
}
# ---------------------------------------------------------------------------
# GPIO bank → controller address (Tegra264 / Thor)
# Source: bootloader/tegra264-gpio.h
# ---------------------------------------------------------------------------
_MAIN_GPIO_BANKS = {
"F", "G", "H", "J", "K", "L", "M", "P", "Q", "R",
"S", "T", "U", "V", "W", "X", "Y", "Z", "AL",
}
_UPHY_GPIO_BANKS = {"A", "B", "C", "D", "E"}
_AON_GPIO_BANKS = {"AA", "BB", "CC", "DD", "EE"}
_FSI_GPIO_BANKS = {"AB", "AC", "AD", "AE", "AF", "AG", "AH", "AJ"}
_BANK_TO_CTRL: dict[str, str] = {}
for _b in _MAIN_GPIO_BANKS:
_BANK_TO_CTRL[_b] = "gpio@ac300000"
for _b in _UPHY_GPIO_BANKS:
_BANK_TO_CTRL[_b] = "gpio@e8300000"
for _b in _AON_GPIO_BANKS:
_BANK_TO_CTRL[_b] = "gpio@8cf00000"
for _b in _FSI_GPIO_BANKS:
_BANK_TO_CTRL[_b] = "gpio@b0320000"
_CTRL_TO_MACRO: dict[str, str] = {
"gpio@ac300000": "TEGRA264_MAIN_GPIO",
"gpio@e8300000": "TEGRA264_UPHY_GPIO",
"gpio@8cf00000": "TEGRA264_AON_GPIO",
"gpio@b0320000": "TEGRA264_FSI_GPIO",
}
# Ordered for deterministic output (matches devkit reference DTSI order)
_CTRL_ORDER = ["gpio@ac300000", "gpio@e8300000", "gpio@8cf00000", "gpio@b0320000"]
# ---------------------------------------------------------------------------
# Pad-voltage voltage string → DTS macro
# ---------------------------------------------------------------------------
_VOLTAGE_MACRO: dict[str, str] = {
"3.3v": "IO_PAD_VOLTAGE_3_3V",
"3.3": "IO_PAD_VOLTAGE_3_3V",
"1.8v": "IO_PAD_VOLTAGE_1_8V",
"1.8": "IO_PAD_VOLTAGE_1_8V",
"1.2v": "IO_PAD_VOLTAGE_1_2V",
"1.2": "IO_PAD_VOLTAGE_1_2V",
}
# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
@dataclass
class _PinIds:
"""Pin identity fields (names, ball, signal)."""
dt_pin_name: str
verilog_name: str
package_ball: str
connector_pin: str
signal_name: str
power_rail: str
customer_usage: str
@dataclass
class _PinElec:
"""Electrical/configuration fields from the XLSM."""
pin_direction: str
gpio_init_val: str
pupd: str
tristate: str
e_input: str
io_voltage: str
por_state: str
@dataclass
class _PinDrive:
"""Drive-related electrical fields (open-drain, drive type, loopback)."""
e_io_od: str
drv_type: str
e_lpbk: str
@dataclass
class _PinDerived:
"""Derived fields populated during parsing."""
sfio_options: list[str] = field(default_factory=list)
resolved_func: str = ""
pad_type: str = ""
pin_group: str = ""
_GROUP_NAMES = ("ids", "elec", "drive", "derived")
@dataclass
class PinRow:
"""One pin's parsed XLSM row, used for DTSI emission + pinmap export."""
ids: _PinIds
elec: _PinElec
drive: _PinDrive
derived: _PinDerived
def __getattr__(self, name: str):
for grp_name in _GROUP_NAMES:
grp = self.__dict__.get(grp_name)
if grp is not None and hasattr(grp, name):
return getattr(grp, name)
raise AttributeError(name)
def __setattr__(self, name: str, value) -> None:
if name in _GROUP_NAMES:
object.__setattr__(self, name, value)
return
for grp_name in _GROUP_NAMES:
grp = self.__dict__.get(grp_name)
if grp is not None and hasattr(grp, name):
setattr(grp, name, value)
return
object.__setattr__(self, name, value)
# ---------------------------------------------------------------------------
# XLSM parsing
# ---------------------------------------------------------------------------
def _s(v) -> str:
"""Cell value → stripped string."""
return "" if v is None else str(v).strip()
def _find_header_row(ws) -> int:
for r in range(1, min(ws.max_row, 25) + 1):
if _s(ws.cell(r, 1).value) == "Pin #":
return r
return -1
def _detect_data_sheet(wb) -> tuple:
"""Return (worksheet, header_row) for the first data sheet found."""
for name in wb.sheetnames:
if name.lower().strip() in _NON_DATA_SHEETS:
continue
ws = wb[name]
hr = _find_header_row(ws)
if hr != -1:
return ws, hr
raise SystemExit("generate_dtsi: no data sheet found in workbook")
def _resolve_func(row_vals: list, cols: dict[str, int]) -> tuple[str, list[str]]:
"""Determine nvidia,function from customer_usage vs mux columns.
Returns (function_string, sfio_options_list).
function_string is lowercased DT function name (e.g. 'i2c7_clk', 'gpio').
"""
cu = _s(row_vals[cols["customer_usage"] - 1]).lower()
mux_keys = ["mux_gpio", "mux_sfio0", "mux_sfio1", "mux_sfio2", "mux_sfio3"]
sfio_opts: list[str] = []
func = ""
for k in mux_keys:
raw = _s(row_vals[cols[k] - 1])
if not raw:
continue
lowered = raw.lower()
# Normalise GPIO entries: "GPIO3_PG.01" → "gpio"
if re.match(r"gpio\d*_", lowered) or lowered.startswith("gpio"):
token = "gpio"
else:
token = lowered
if token not in sfio_opts:
sfio_opts.append(token)
# Match customer_usage to this mux slot
if not func:
cu_norm = re.sub(r"[^a-z0-9]", "_", cu)
tok_norm = re.sub(r"[^a-z0-9]", "_", token)
if cu_norm == tok_norm or cu_norm in tok_norm or tok_norm in cu_norm:
func = token
# Pins with no mux/sfio options (only mux_unused set) → reserved function.
if not sfio_opts:
return "rsvd0", []
# Fallback: if customer_usage starts with "unused" → rsvd0 (pin not used on
# this carrier). Mirrors the Excel VBA macro which outputs rsvd0 for all
# "unused_*" pins even when the silicon exposes a non-reserved SFIO option.
if not func:
if cu.startswith("unused") or cu == "":
func = "rsvd0"
else:
func = re.sub(r"[^a-z0-9_]", "_", cu).strip("_") or "gpio"
return func, sfio_opts
def _parse_gpio_bank_index(dt_pin_name: str) -> tuple[str, int] | None:
"""Extract (BANK, index) from DT pin name suffix.
Examples:
gen7_i2c_scl_pg1 → ("G", 1)
soc_gpio00_paa0 → ("AA", 0)
hdmi_cec_paa6 → ("AA", 6)
soc_gpio61_pal5 → ("AL", 5)
"""
# Match trailing _p<bank><index> where bank is 1-2 letters
m = re.search(r"_p([a-z]{1,2})(\d+)$", dt_pin_name.lower())
if not m:
return None
bank = m.group(1).upper()
idx = int(m.group(2))
return bank, idx
@dataclass
class _ParsedRowInputs:
"""Already-computed fields passed to _build_pinrow."""
dt_pin: str
verilog: str
ball: str
pad_type: str
sfio_opts: list[str]
func: str
def _build_pinrow(row_vals, cols, parsed: _ParsedRowInputs) -> PinRow:
"""Construct a PinRow from a raw XLSM data row + parsed metadata."""
return PinRow(
ids=_PinIds(
dt_pin_name=parsed.dt_pin,
verilog_name=parsed.verilog,
package_ball=parsed.ball,
connector_pin=_s(row_vals[cols["connector_pin"] - 1]),
signal_name=_s(row_vals[cols["signal_name"] - 1]),
power_rail=_s(row_vals[cols["power_rail"] - 1]).lower(),
customer_usage=_s(row_vals[cols["customer_usage"] - 1]),
),
elec=_PinElec(
pin_direction=_s(row_vals[cols["pin_direction"] - 1]),
gpio_init_val=_s(row_vals[cols["gpio_init_val"] - 1]).lower(),
pupd=_s(row_vals[cols["pupd"] - 1]),
tristate=_s(row_vals[cols["tristate"] - 1]),
e_input=_s(row_vals[cols["e_input"] - 1]),
io_voltage=_s(row_vals[cols["io_voltage"] - 1]),
por_state=_s(row_vals[cols["por_state"] - 1]),
),
drive=_PinDrive(
e_io_od=_s(row_vals[cols["e_io_od"] - 1]),
drv_type=_s(row_vals[cols["drv_type"] - 1]),
e_lpbk=_s(row_vals[cols["e_lpbk"] - 1]),
),
derived=_PinDerived(
sfio_options=parsed.sfio_opts,
resolved_func=parsed.func,
pad_type=parsed.pad_type,
pin_group=_s(row_vals[cols["pin_group"] - 1]),
),
)
def _parse_one_row(row_vals: list, cols: dict[str, int]) -> PinRow | None:
"""Process a single raw XLSM data row. Returns None if the row is skipped."""
ball = _s(row_vals[cols["package_ball"] - 1])
verilog = _s(row_vals[cols["verilog_name"] - 1])
if not ball or not verilog:
return None
pad_type = _s(row_vals[cols["pad_type"] - 1])
is_mpio = pad_type.startswith(_MPIO_PAD_PREFIXES)
has_mux = any(
_s(row_vals[cols[k] - 1])
for k in ("mux_gpio", "mux_sfio0", "mux_sfio1", "mux_sfio2", "mux_sfio3")
)
has_mux_unused = bool(_s(row_vals[cols["mux_unused"] - 1]))
if not has_mux and not (is_mpio and has_mux_unused):
return None
dt_pin = _s(row_vals[cols["dt_pin_name"] - 1]).lower().replace(" ", "_")
if not dt_pin:
dt_pin = verilog.lower().replace(" ", "_")
func, sfio_opts = _resolve_func(row_vals, cols)
parsed = _ParsedRowInputs(
dt_pin=dt_pin, verilog=verilog, ball=ball,
pad_type=pad_type, sfio_opts=sfio_opts, func=func,
)
return _build_pinrow(row_vals, cols, parsed)
def parse_xlsm(xlsm_path: pathlib.Path) -> list[PinRow]:
"""Parse all data rows from the XLSM; return list of PinRow."""
if openpyxl is None:
raise ImportError("openpyxl is required to parse XLSM files")
wb = openpyxl.load_workbook(str(xlsm_path), data_only=True, keep_vba=False)
ws, header_row = _detect_data_sheet(wb)
cols = THOR_COLS
max_col = max(cols.values()) + 2
rows: list[PinRow] = []
for r in range(header_row + 1, ws.max_row + 1):
row_vals = [ws.cell(r, c).value for c in range(1, max_col)]
pin = _parse_one_row(row_vals, cols)
if pin is not None:
rows.append(pin)
wb.close()
print(f"parsed {len(rows)} pin rows from {xlsm_path.name}", file=sys.stderr)
return rows
# ---------------------------------------------------------------------------
# DTSI generation helpers
# ---------------------------------------------------------------------------
_SPDX_HEADER = (
"/*\n"
" * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION"
" & AFFILIATES. All rights reserved.\n"
) + """\
* SPDX-License-Identifier: BSD-3-Clause
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
"""
def _tegra_pin_val(raw: str, enable_val: str = "TEGRA_PIN_ENABLE",
disable_val: str = "TEGRA_PIN_DISABLE") -> str:
"""Map 'Enable'/'Disable'/'' → DTS macro string."""
r = raw.strip().lower()
if r in ("enable", "1", "yes", "true"):
return enable_val
if r in ("disable", "0", "no", "false"):
return disable_val
# Default safe fallback
return disable_val
def _pull_val(pupd: str) -> str:
r = pupd.strip().lower()
if "up" in r:
return "TEGRA_PIN_PULL_UP"
if "down" in r:
return "TEGRA_PIN_PULL_DOWN"
return "TEGRA_PIN_PULL_NONE"
def _drv_type_val(drv_type: str) -> str:
# AY column values:
# "Enable" / "High" → TEGRA_PIN_2X_DRIVER (BDGPIO high-drive)
# "DEF_1X" → TEGRA_PIN_DEFAULT_DRIVE_1X (BDSDMEM pads)
# "Disable" / "" → TEGRA_PIN_1X_DRIVER (BDGPIO normal)
r = drv_type.strip().lower()
if r in ("enable", "1", "high"):
return "TEGRA_PIN_2X_DRIVER"
if r in ("def_1x", "default_1x", "default"):
return "TEGRA_PIN_DEFAULT_DRIVE_1X"
return "TEGRA_PIN_1X_DRIVER"
def _tristate_for_dir(pin: PinRow) -> str:
"""Tristate is ENABLE for Unused pins or when explicitly set.
The XLSM tristate column (col 35) carries 'TRISTATE' or 'NORMAL' — map
those to ENABLE / DISABLE respectively.
"""
d = pin.pin_direction.strip().lower()
if d == "unused":
return "TEGRA_PIN_ENABLE"
# Use the sheet's tristate column if populated
ts = pin.tristate.strip().upper()
if ts == "TRISTATE":
return "TEGRA_PIN_ENABLE"
# "NORMAL" → DISABLE (also the fallback for empty)
return "TEGRA_PIN_DISABLE"
def _einput_for_dir(pin: PinRow) -> str:
"""Enable-input: ENABLE for Input/Bidirectional, DISABLE for Output/Unused."""
d = pin.pin_direction.strip().lower()
if d in ("input", "bidirectional", "bidir"):
return "TEGRA_PIN_ENABLE"
if pin.e_input:
return _tegra_pin_val(pin.e_input)
return "TEGRA_PIN_DISABLE"
# ---------------------------------------------------------------------------
# Pinmux DTSI
# ---------------------------------------------------------------------------
_MPIO_PAD_PREFIXES = ("BDGPIO", "BDSDMEM", "BDDPAUX")
def generate_pinmux_dtsi(pins: list[PinRow], carrier_name: str,
xlsm_name: str) -> str:
"""Render the pinmux DTSI for the given parsed pins and carrier name."""
lines: list[str] = []
lines.append(f"/*This dtsi file was generated by generate_dtsi.py from {xlsm_name} */")
lines.append(_SPDX_HEADER)
lines.append("")
lines.append('#include "t264-pinctrl-tegra.h"')
lines.append("")
lines.append(f'#include "./tegra264-mb1-bct-gpio-{carrier_name}.dtsi"')
lines.append("")
lines.append("pinmux@ac281000 {")
lines.append("\t\tcommon {")
lines.append("\t\t\t/* SFIO Pin Configuration */")
# Only emit MPIO pads (BDGPIO*, BDSDMEM*, BDDPAUX*) that have a pin_group
# (col 32) — same filter as the Excel VBA macro. Pins without a pin_group
# are module-internal special pads (JTAG_TDO, PMU_INT_N, SOC_PWR_REQ,
# SDMMC1_COMP) that the VBA macro does not emit.
mpio_pins = [p for p in pins
if p.pad_type.startswith(_MPIO_PAD_PREFIXES) and p.pin_group]
for pin in mpio_pins:
is_sdmem = pin.pad_type.startswith("BDSDMEM")
lines.append(f"\t\t\t{pin.dt_pin_name} {{")
lines.append(f'\t\t\t\tnvidia,pins = "{pin.dt_pin_name}";')
lines.append(f'\t\t\t\tnvidia,function = "{pin.resolved_func}";')
lines.append(f"\t\t\t\tnvidia,pull = <{_pull_val(pin.pupd)}>;")
lines.append(f"\t\t\t\tnvidia,tristate = <{_tristate_for_dir(pin)}>;")
lines.append(f"\t\t\t\tnvidia,enable-input = <{_einput_for_dir(pin)}>;")
lines.append(f"\t\t\t\tnvidia,drv-type = <{_drv_type_val(pin.drv_type)}>;")
if not is_sdmem:
# BDSDMEM pads have no e-io-od property in the reference DTSI
lines.append(f"\t\t\t\tnvidia,e-io-od = <{_tegra_pin_val(pin.e_io_od)}>;")
lines.append(f"\t\t\t\tnvidia,e-lpbk = <{_tegra_pin_val(pin.e_lpbk)}>;")
lines.append("\t\t\t};")
lines.append("")
lines.append("\t\t};")
lines.append("};")
lines.append("")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# GPIO DTSI
# ---------------------------------------------------------------------------
def _collect_gpio_buckets(pins: list[PinRow]) -> tuple[dict, dict, dict]:
"""Bucket gpio pins by controller and direction (input/out-low/out-high)."""
ctrl_input: dict[str, list[str]] = defaultdict(list)
ctrl_outlow: dict[str, list[str]] = defaultdict(list)
ctrl_outhigh: dict[str, list[str]] = defaultdict(list)
for pin in pins:
if pin.resolved_func != "gpio":
continue
parsed = _parse_gpio_bank_index(pin.dt_pin_name)
if not parsed:
continue
bank, idx = parsed
ctrl = _BANK_TO_CTRL.get(bank)
if not ctrl:
continue
entry = f"\t\t\t{_CTRL_TO_MACRO[ctrl]}({bank}, {idx})"
d = pin.pin_direction.strip().lower()
iv = pin.gpio_init_val.strip().lower()
if d == "input" or d in ("bidirectional", "bidir"):
ctrl_input[ctrl].append(entry)
elif d == "output":
(ctrl_outhigh if iv == "1" else ctrl_outlow)[ctrl].append(entry)
return ctrl_input, ctrl_outlow, ctrl_outhigh
def _emit_gpio_ctrl_block(ctrl: str, ctrl_input: dict, ctrl_outlow: dict,
ctrl_outhigh: dict) -> list[str]:
"""Emit DTSI lines for a single gpio controller's default block."""
lines = [f"{ctrl} {{", "\t default {"]
if ctrl_input.get(ctrl):
lines.append("\t\tgpio-input = <")
lines.extend(ctrl_input[ctrl])
lines.append("\t\t\t>;")
lines.append("\t\tgpio-output-low = <")
lines.extend(ctrl_outlow.get(ctrl, []))
lines.append("\t\t\t>;")
lines.append("\t\tgpio-output-high = <")
lines.extend(ctrl_outhigh.get(ctrl, []))
lines.append("\t\t\t>;")
lines.append("\t};")
lines.append("};")
return lines
def generate_gpio_dtsi(pins: list[PinRow], carrier_name: str,
xlsm_name: str) -> str:
"""Render the gpio default-state DTSI for the given parsed pins."""
del carrier_name # part of the public DTSI-generator signature
ctrl_input, ctrl_outlow, ctrl_outhigh = _collect_gpio_buckets(pins)
lines: list[str] = [
f"/*This dtsi file was generated by generate_dtsi.py from {xlsm_name} */",
_SPDX_HEADER,
'#include "tegra264-gpio.h"',
"",
]
for ctrl in _CTRL_ORDER:
if not (ctrl_input.get(ctrl) or ctrl_outlow.get(ctrl)
or ctrl_outhigh.get(ctrl)):
continue
lines.extend(_emit_gpio_ctrl_block(
ctrl, ctrl_input, ctrl_outlow, ctrl_outhigh))
lines.append("")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Padvoltage DTSI
# ---------------------------------------------------------------------------
def parse_pad_voltage_groups(xlsm_path: pathlib.Path) -> dict[str, str]:
"""Read pad-group voltage settings from XLSM section header rows.
The Excel macro reads VDDIO_G / VDDIO_J / VDDIO_P from section-header rows
(col 3 = group name, col 61 = voltage) rather than per-pin io_voltage.
When a cell contains a compound value like "1.8V/3.3V", the last listed
voltage is used as the default (matches reference DTSI behaviour).
Only the three Thor PMC io-pad group names that appear in the DT binding
are returned: vddio_g, vddio_j, vddio_p. All other VDDIO_* section
headers (VDDIO_A, VDDIO_B, VDDIO_C … VDDIO_DDR*) are not PMC pad nodes
and must not appear in the padvoltage DTSI.
"""
allowed_groups = {"VDDIO_G", "VDDIO_J", "VDDIO_P"}
if openpyxl is None:
raise ImportError("openpyxl is required to parse XLSM files")
wb = openpyxl.load_workbook(str(xlsm_path), data_only=True, keep_vba=False)
ws, _ = _detect_data_sheet(wb)
groups: dict[str, str] = {}
for row in ws.iter_rows(min_row=1, values_only=True):
name = _s(row[2]) # col 3
volt = _s(row[60]) # col 61
if not name or not volt:
continue
name_upper = name.strip().upper()
if name_upper not in allowed_groups:
continue
for opt in reversed(volt.split("/")):
macro = _VOLTAGE_MACRO.get(opt.strip().lower())
if macro:
groups[name_upper.lower()] = macro
break
wb.close()
return groups
def generate_padvoltage_dtsi(pins: list[PinRow], carrier_name: str,
xlsm_name: str,
pad_voltage_groups: dict[str, str] | None = None
) -> str:
"""Generate the padvoltage DTSI.
When `pad_voltage_groups` is provided (from `parse_pad_voltage_groups`),
use those group-level settings - matching how the Excel macro works.
Falls back to per-pin io_voltage scanning when not provided.
"""
del carrier_name # part of the public DTSI-generator signature
if pad_voltage_groups is not None:
rail_voltage = pad_voltage_groups
else:
rail_voltage = {}
for pin in pins:
if not pin.power_rail or not pin.io_voltage:
continue
rail = pin.power_rail.lower().strip()
for opt in reversed(pin.io_voltage.split("/")):
macro = _VOLTAGE_MACRO.get(opt.strip().lower())
if macro and rail:
rail_voltage[rail] = macro
break
lines: list[str] = []
lines.append(f"/*This dtsi file was generated by generate_dtsi.py from {xlsm_name} */")
lines.append(_SPDX_HEADER)
lines.append("#define IO_PAD_VOLTAGE_1_2V 1200000")
lines.append("#define IO_PAD_VOLTAGE_1_8V 1800000")
lines.append("#define IO_PAD_VOLTAGE_3_3V 3300000")
lines.append("")
lines.append("")
lines.append("pmc@8c800000 {")
lines.append("\tio-pad-defaults {")
for rail in sorted(rail_voltage):
lines.append(f"\t\t{rail} {{")
lines.append(f"\t\t\tnvidia,io-pad-init-voltage = <{rail_voltage[rail]}>;")
lines.append("\t\t};")
lines.append("")
lines.append("\t};")
lines.append("};")
lines.append("")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
_DIR_TITLE_MAP = {
"input": "Input", "output": "Output",
"bidirectional": "Bidirectional", "bidir": "Bidirectional",
"unused": "Unused",
}
_PULL_TITLE_MAP = {"none": "None", "pull-up": "Pull-Up", "pull-down": "Pull-Down"}
_DRV_TITLE_MAP = {"normal": "Disable", "high": "Enable"}
def _apply_direction_state(pin: PinRow, direction: str, init_state: str) -> None:
"""Update pin direction / enable-input / tristate from an edit."""
if direction in _DIR_TITLE_MAP:
pin.pin_direction = _DIR_TITLE_MAP[direction]
if direction in ("input", "bidirectional"):
pin.e_input = "Enable"
pin.tristate = "Disable"
elif direction == "output":
pin.e_input = "Disable"
pin.tristate = "Disable"
elif direction == "unused":
pin.e_input = "Disable"
pin.tristate = "Enable"
if init_state == "hi-z":
pin.tristate = "Enable"
def _apply_electrical(pin: PinRow, edit: dict) -> None:
"""Apply pull / drv / open_drain / loopback overrides to a pin."""
pull = (edit.get("pull") or "").lower()
if pull in _PULL_TITLE_MAP:
pin.pupd = _PULL_TITLE_MAP[pull]
drv = (edit.get("drive_type") or "").lower()
if drv in _DRV_TITLE_MAP:
pin.drv_type = _DRV_TITLE_MAP[drv]
od = (edit.get("open_drain") or "").lower()
if od in ("enable", "disable"):
pin.e_io_od = od.capitalize()
lb = (edit.get("loopback") or "").lower()
if lb in ("enable", "disable"):
pin.e_lpbk = lb.capitalize()
def _apply_one_edit(pin: PinRow, edit: dict) -> PinRow:
"""Return a copy of `pin` with the user's edit baked in."""
pin = copy.copy(pin)
sfio = edit.get("sfio", pin.resolved_func)
pin.resolved_func = sfio
if sfio not in pin.sfio_options:
pin.sfio_options = [sfio] + list(pin.sfio_options)
direction = (edit.get("direction") or "").lower()
init_state = (edit.get("initial_state") or "n/a").lower()
if sfio == "gpio":
pin.gpio_init_val = {"high": "1", "low": "0"}.get(init_state, "z")
_apply_direction_state(pin, direction, init_state)
_apply_electrical(pin, edit)
return pin
def apply_pin_edits(pins: list[PinRow], edits: list[dict]) -> list[PinRow]:
"""Override PinRow fields with per-pin edits from session.pinmux.pin_edits."""
edit_by_pin = {e["pin"]: e for e in edits}
result: list[PinRow] = []
for pin in pins:
edit = edit_by_pin.get(pin.dt_pin_name)
if edit:
pin = _apply_one_edit(pin, edit)
result.append(pin)
return result
def build_pinmap_records(pins: list[PinRow]) -> list[dict]:
"""Convert PinRow list to pinmap-compatible dicts for modify_pinmux lookup.
The sfio field uses the ``mode=name`` encoding that ``_sfio_options()``
in modify_pinmux.py expects:
- ``"gpio=gpio"`` → mode gpio → emits ``"gpio"``
- ``"sfio0=i2c7_clk"`` → mode sfio0 → emits ``"i2c7_clk"``
The gpio bank/idx is NOT encoded in the sfio string here; modify_pinmux
derives it from the DT pin name suffix (``_p<bank><idx>``), which is
present in every Thor dt_pin_name produced by the XLSM.
"""
records: list[dict] = []
for pin in pins:
# Build sfio list in mode=name format.
sfio_entries: list[str] = []
sfio_idx = 0
for opt in pin.sfio_options:
if opt == "gpio":
sfio_entries.append("gpio=gpio")
else:
sfio_entries.append(f"sfio{sfio_idx}={opt}")
sfio_idx += 1
# Normalise direction to single-char convention used by existing
# pinmap rows (I/O/B/U) so _print_row_details can display it.
d = pin.pin_direction.strip().lower()
dir_char = {"input": "I", "output": "O", "bidirectional": "B",
"bidir": "B", "unused": "U"}.get(d, d[:1].upper() if d else "")
# Normalise XLSM raw strings to the values set-pin expects.
pupd_raw = pin.pupd.strip().lower()
pupd_norm = ("pull-up" if "up" in pupd_raw
else "pull-down" if "down" in pupd_raw
else "none")
drv_norm = "high" if pin.drv_type.strip().lower() == "enable" else "normal"
od_norm = "enable" if pin.e_io_od.strip().lower() == "enable" else "disable"
lpbk_norm = "enable" if pin.e_lpbk.strip().lower() == "enable" else "disable"
# A pad is software-configurable (pull/drv/od/lpbk editable) only when
# its pad_type starts with "BD" or "BI" — i.e. BDGPIO*, BDDPAUX*,
# BDSDMEM*, BIRST*, BICLK32K*, BIPOD*. LP5XA_*, UPHYDS_*, DP_SINGLE_*,
# BDMIPI16X_*, BDUSB2_*, OSCI27_* have empty attribute fields and are
# fixed-function pads; per-pin questions 4-6 do not apply to them.
configurable = pin.pad_type.upper().startswith(("BD", "BI"))
records.append({
"pin": pin.dt_pin_name,
"ball": pin.package_ball,
"connector_pin": pin.connector_pin,
"verilog_name": pin.verilog_name,
"signal_name": pin.signal_name,
"customer_usage": pin.customer_usage,
"direction": dir_char,
"category": "other",
"sfio": sfio_entries,
"evidence": [],
"power_rail": pin.power_rail,
"pad_type": pin.pad_type,
"por_state": pin.por_state,
"configurable": configurable,
"pupd": pupd_norm if configurable else "",
"drv_type": drv_norm if configurable else "",
"e_io_od": od_norm if configurable else "",
"e_lpbk": lpbk_norm if configurable else "",
})
return records
def main(argv=None) -> int:
"""CLI entry point: parse the XLSM and write the 3 DTSI files."""
p = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
p.add_argument("--xlsm", required=True, type=pathlib.Path,
help="Path to the Jetson Thor pinmux template .xlsm")
p.add_argument("--out-dir", required=True, type=pathlib.Path,
help="Output directory for the three .dtsi files")
p.add_argument("--carrier-name", required=True,
help="Carrier suffix for filenames, e.g. p3834-xxxx-p4071-0000")
p.add_argument("--dry-run", action="store_true",
help="Parse and report counts but do not write files")
args = p.parse_args(argv)
xlsm = args.xlsm.expanduser().resolve()
if not xlsm.exists():
print(f"error: xlsm not found: {xlsm}", file=sys.stderr)
return 1
out_dir = args.out_dir.expanduser().resolve()
if not args.dry_run:
out_dir.mkdir(parents=True, exist_ok=True)
pins = parse_xlsm(xlsm)
gpio_pins = [p for p in pins if p.resolved_func == "gpio"]
sfio_pins = [p for p in pins if p.resolved_func != "gpio"]
rails = {p.power_rail for p in pins if p.power_rail and p.io_voltage}
print(f" total pins : {len(pins)}", file=sys.stderr)
print(f" gpio pins : {len(gpio_pins)}", file=sys.stderr)
print(f" sfio pins : {len(sfio_pins)}", file=sys.stderr)
print(f" pad rails : {len(rails)} ({', '.join(sorted(rails))})", file=sys.stderr)
if args.dry_run:
print("dry-run: no files written", file=sys.stderr)
return 0
cn = args.carrier_name
xlsm_name = xlsm.name
files = {
out_dir / f"tegra264-mb1-bct-pinmux-{cn}.dtsi":
generate_pinmux_dtsi(pins, cn, xlsm_name),
out_dir / f"tegra264-mb1-bct-gpio-{cn}.dtsi":
generate_gpio_dtsi(pins, cn, xlsm_name),
out_dir / f"tegra264-mb1-bct-padvoltage-{cn}.dtsi":
generate_padvoltage_dtsi(pins, cn, xlsm_name),
}
for path, content in files.items():
path.write_text(content, encoding="utf-8")
print(f"wrote {path}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
Requirements and compatibility
Written for: Claude Code
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Names Claude Code tools, Needs Python
Security
What our scanner noticed· Medium risk
- Large base64-encoded blobs
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Jetson Team
- Publisher
- NVIDIA (Official)
- Distributed by
- NVIDIA
- Source
- NVIDIA/skills · skills/jetson-customize-pinmux · commit fce3cca
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE-APACHE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Jetson Team or NVIDIA.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download jetson-customize-pinmux.zip