SKILL.md
Modify nvfancontrol Fan Profile (BSP-side)
Purpose
Edit the per-board nvfancontrol configuration so the device boots with the desired fan curve / control mode / governor / default profile. BSP-side only — all writes land in the overlay tracker, the upstream bsp_image copy is read-only.
This skill handles BSP-side edits to the per-board nvfancontrol configuration file: adding profiles, removing profiles, editing the temp → PWM / RPM curves, changing the boot default profile / control mode / governor, and listing defined profiles. Applies on Jetson / Tegra platforms (T234 Orin, T264 Thor).
File format (canonical, per the BSP file header)
POLLING_INTERVAL <seconds>
<FAN <index>>
TMARGIN <ENABLED|DISABLED>
FAN_GOVERNOR <type> {
STEP_SIZE <int>
}
FAN_CONTROL <close_loop|open_loop> {
RPM_TOLERANCE <rpm>
}
FAN_PROFILE <name> {
# TEMP HYST PWM RPM
<T0> <H0> <P0> <R0>
...
}
FAN_PROFILE <name> { ... } # one or more profiles
THERMAL_GROUP <id> {
GROUP_MAX_TEMP <C>
# zone-name <coeffs csv> <max-temp>
<zone> <coeffs> <max-temp>
...
}
FAN_DEFAULT_CONTROL <close_loop|open_loop>
FAN_DEFAULT_PROFILE <name>
FAN_DEFAULT_GOVERNOR <type>
KICKSTART_PWM <0..255>
Rules:
- Profile curve tuples are 4-column:
TEMP HYST PWM RPM. Sort points by ascendingTEMP; the daemon interpolates between them. PWMis0..255(8-bit duty cycle);RPMis the close-loop target speed. A trailing0 0row at the high end pins the fan off aboveGROUPMAXTEMP.HYSTis hysteresis (°C) at that point — the controller waitsHYSTdegrees of cooling before stepping the curve down.FANDEFAULTPROFILEmust reference an existingFAN_PROFILEblock in the same<FAN N>. nvfancontrol fails to start if the default names a missing profile.FANDEFAULTCONTROL=closeloop(drives toward target RPM, requires tach) oropenloop(writes PWM directly).FANDEFAULTGOVERNOR=cont(continuous interpolation) and other family-specific values; copy verbatim from existing per-board files when introducing one.THERMAL_GROUPmaps thermal zones to the controller's input. Coefficients are a 20-element CSV — copy verbatim from existing entries; values vary by chip family and zone.<FAN N>is one block per fan index (typical:<FAN 1>). Block boundaries<...>and{...}are strict; preserve indentation matching neighboring lines.- Curves are characterised, not invented. Add or edit curves from real thermal-acoustic data for the platform; do not interpolate from neighbouring profiles or copy across chip families.
Prerequisites
Resolve the active profile per [../../context/target-platform-contract.md](../../context/target-platform-contract.md). Refuse and route in these cases:
| Condition | Refuse with |
|---|---|
No active profile, or active: NA |
Route to /jetson-set-target or /jetson-init-target. |
Profile lacks bsp_image: block |
Route to /jetson-init-image. |
<bspimage.rootpath>/LinuxforTegra/ missing |
Route to /jetson-init-image. |
<source.rootpath>/Linuxfor_Tegra/ missing or not a git repo |
Route to /jetson-init-source. |
Resolve paths:
<bspimage.rootpath>frombspimage.rootpath:if present, else<workspace>/Image.<source.rootpath>fromsource.rootpath:if present, else<workspace>/Source.
<bspimage.rootpath> is read-only for this skill; every write lands under <source.root_path> (the overlay tracker). This is the workflow invariant in [../../context/bsp-customization-workflow.md#workflow-invariants](../../context/bsp-customization-workflow.md#workflow-invariants) — hand-editing upstream silently destroys the diff trail and makes /jetson-promote-image a noop.
The per-board file
The conf this skill edits has the relative path:
Linux_for_Tegra/rootfs/etc/nvpower/nvfancontrol/nvfancontrol_<active-sku>.conf
It lives in two roots; the skill walks both:
| Role | Location | Skill writes? |
|---|---|---|
| Detection + pristine source | <bspimage.rootpath>/LinuxforTegra/rootfs/etc/nvpower/nvfancontrol/ |
no — read-only |
| Overlay edit target + git commit | <source.rootpath>/Linuxfor_Tegra/rootfs/etc/nvpower/nvfancontrol/ |
yes |
Subsequent sections refer to the per-board file to mean the overlay copy under <source.rootpath>. Operations 1–4 all read, edit, and save against that overlay copy. The <bspimage.root_path> copy is read once during the "Resolving <active-sku>" detection step and once during the [Overlay edit recipe](#overlay-edit-recipe-apply-before-any-operation)'s pristine-import step, then never touched again.
Resolving <active-sku> — which file to edit
Filename conventions vary per product family:
nvfancontrol<module.id><module.sku>.conf— most Orin parts (e.g.nvfancontrolp37670000.conf,nvfancontrolp37010008.conf).nvfancontrol<module.id><module.sku><carrier.id><carrier.sku>.conf— Thor variants where the carrier disambiguates (e.g.nvfancontrolp38340008p40710000.conf).nvfancontrol<carrier><sku><rev>.conf— IGX revision variants (e.g.nvfancontrolp37400002b01.conf).
The nvfancontrol daemon resolves the right file at startup based on the booted hardware's DT compatible plus board IDs (no helper script — it's done inside the binary). To map BSP-side without a running target, against <bspimage.rootpath>:
- List
<bspimage.rootpath>/LinuxforTegra/rootfs/etc/nvpower/nvfancontrol/. - Filter to filenames that contain
<module.id>_<module.sku>from the active profile. - If multiple candidates remain, refine with
<carrier.id>_<carrier.sku>and (when relevant) the carrier revision tag. - If still ambiguous, run the per-board flash conf dispatch chain to read the kernel DTB's
compatibleand pick the file whose name aligns with the resolved board / carrier. - Verify the chosen file actually exists under
<bspimage.rootpath>/LinuxforTegra/rootfs/etc/nvpower/nvfancontrol/.
Don't blindly compose a filename — the naming convention varies by product family.
Propagation set — confs to keep in sync
The active-SKU file is rarely the only conf that should carry a customization. After editing it, apply the same edit to every sibling in the propagation set so the change survives regardless of which module / baseboard SKU is booted:
- The reference platform's nvfancontrol conf — the upstream conf the active file was forked from (resolve via
reference_devkitin the active profile, orjetson-derive-carrierfork ancestry). For a BSP that contains only the reference (no derived carriers), this is the same file as the active and the rule reduces to a no-op. - Every carrier-derived nvfancontrol conf — each
nvfancontrol_*.confproduced byjetson-derive-carrierfor a custom carrier on top of the same module SKU.
"Apply the same edit" ≠ blanket file copy. Port the changed FANPROFILE / control lines into each sibling; preserve every other line. Sibling confs often hold carrier-specific deltas (different THERMALGROUP coefficients for a different thermal solution, different tach RPM ceilings) that must stay intact — a blanket overwrite would mis-tune the fan on those carriers. Blanket-copying is safe only when both confs were byte-identical before the edit.
Overlay edit recipe (apply before any Operation)
Follow the canonical [Off-skill edits recipe](../../context/bsp-customization-workflow.md#off-skill-edits) in the workflow doc — pristine import + customization commit pair, both gated by the preview gate. Apply once per run, covering every per-board file the run touches (the active conf plus every sibling in the [Propagation set](#propagation-set--confs-to-keep-in-sync)).
Concrete substitutions for this skill:
<rel>/<file>isrootfs/etc/nvpower/nvfancontrol/<conf>.- Suggested pristine-import message:
import pristine: <comma-separated rel paths of imported confs>, body Source: <bspimage.rootpath>/LinuxforTegra/ (BSP <bsp_image.version>).
- Suggested customization-commit header:
jetson-customize-fan: <summary>, body lines like nvfancontrolp37670000.conf: added FANPROFILE static (open-loop, PWM=255 flat), FANDEFAULTCONTROL closeloop -> openloop, FANDEFAULT_PROFILE quiet -> static.
Instructions
Pick the operation that matches the user's intent and follow the matching subsection. All write-side operations (1–4) must first apply the [Overlay edit recipe](#overlay-edit-recipe-apply-before-any-operation).
- Operation 1 — Add a new fan profile.
- Operation 2 — Remove a fan profile.
- Operation 3 — Edit an existing fan profile (curve, hysteresis).
- Operation 4 — Change the boot default (control / profile / governor).
- Operation 5 — List defined fan profiles (read-only).
After any write-side operation, run the Deploy chain (## Deploy) to land the change on the device.
Examples
Add an aggressive profile to a P3767-0000 (Orin Nano dev kit) target and pin it as the boot default:
/jetson-customize-fan
> add a profile called "aggressive" with the curve {0:255, 40:150, 80:0}
> set FAN_DEFAULT_PROFILE to aggressive
Soften the fan ramp on the existing quiet profile (raise HYST, lower mid-PWM):
/jetson-customize-fan
> edit FAN_PROFILE quiet — raise HYST to 5 from 30 °C up, drop PWM at 60 °C to 80
List which profiles the active SKU currently defines and which is the boot default:
/jetson-customize-fan
> list defined fan profiles
Operation 1 — Add a new fan profile
Apply the [Overlay edit recipe](#overlay-edit-recipe-apply-before-any-operation) first.
- In the per-board file, inside the
<FAN N>block, add a newFAN_PROFILE <name> { ... }between existing profiles. Daemon parse order doesn't matter, but grouping with siblings keeps the file scannable. - Pick
<name>lowercase, no whitespace (e.g.quiet,cool,aggressive). - Fill the curve table — 4-column
TEMP HYST PWM RPMtuples, ascendingTEMP. End with at least one row aboveGROUPMAXTEMPso behavior at over-temp is defined.
Example skeleton:
FAN_PROFILE aggressive {
#TEMP HYST PWM RPM
0 0 255 6000
20 2 255 6000
40 2 150 3500
60 2 50 1500
80 0 0 0
105 0 0 0
}
Validate (see Rules):
- Every
TEMPlies within0..GROUPMAXTEMPof theTHERMAL_GROUP. PWM∈[0, 255];RPM≤ the platform's tach-reported max (varies by fan part).- Tuples are sorted ascending by
TEMP.
If you intend the new profile to be the boot default, follow Operation 4.
Operation 2 — Remove a fan profile
Apply the [Overlay edit recipe](#overlay-edit-recipe-apply-before-any-operation) first.
- In the per-board file, delete the entire
FAN_PROFILE <name> { ... }block including all curve rows and the closing}. - If the deleted
<name>matches the trailingFANDEFAULTPROFILE, pointFANDEFAULTPROFILEat a remaining profile — otherwise the daemon fails to start. - Search for hard-coded references in the rootfs before declaring the change safe:
``bash grep -rn "nvfancontrol.*profile\|FANPROFILE" \ Linuxfor_Tegra/rootfs/etc 2>/dev/null ``
The remaining profiles do not need renaming.
Operation 3 — Edit an existing fan profile
Apply the [Overlay edit recipe](#overlay-edit-recipe-apply-before-any-operation) first.
- In the per-board file, modify the curve rows of the target
FAN_PROFILE <name>. Keep the 4-columnTEMP HYST PWM RPMshape. - Maintain ascending
TEMPordering; insert or remove rows as needed. - Adjust
HYSTto tune anti-oscillation: too low → fan thrashes near a curve point; too high → fan lags reality. - Validate per Operation 1's rules.
Edits to KICKSTARTPWM, RPMTOLERANCE (inside FANCONTROL), or STEPSIZE (inside FAN_GOVERNOR) sit outside the profiles but tune the same fan; same per-board file.
Operation 4 — Change the boot default
Apply the [Overlay edit recipe](#overlay-edit-recipe-apply-before-any-operation) first.
Edit the trailing default lines inside the <FAN N> block:
FAN_DEFAULT_CONTROL <close_loop|open_loop>
FAN_DEFAULT_PROFILE <name>
FAN_DEFAULT_GOVERNOR <type>
FANDEFAULTPROFILE must reference an existing FANPROFILE in the same <FAN N> block. FANDEFAULTCONTROL and FANDEFAULT_GOVERNOR must reference values the binary supports — copy from existing per-board files when changing.
Operation 5 — List defined fan profiles
This is a read-only operation; no overlay-tracker setup is needed. Run against whichever copy you want to inspect (<bspimage.rootpath>/... for the pristine state, <source.root_path>/... for the post-edit state):
grep -E '^[[:space:]]*FAN_PROFILE ' <per-board file>
grep -E '^[[:space:]]*FAN_DEFAULT_' <per-board file>
The first prints every profile name in the file; the second prints the boot defaults (control / profile / governor).
Limitations
- BSP-side scope only — this skill never touches a running target's
/etc/nvpower/nvfancontrol/ directly. Live tuning requires reboot via Deploy, or the side-channel scp + systemctl restart flow described in ## Deploy.
- Edits land in the overlay copy under
<source.root_path>only;
the <bspimage.rootpath> copy is read-only and is rewritten by /jetson-promote-image. Hand-editing bsp_image is silently lost on the next /jetson-init-image re-extract.
- Curve tuples must reflect characterised thermal-acoustic data for
the platform — this skill does not interpolate or copy curves across chip families.
- Propagation across sibling carriers (same module SKU) is partial
by design: only the changed FANPROFILE / control lines are ported, never a blanket file overwrite, since sibling confs may hold carrier-specific THERMALGROUP / RPM ceilings.
- Curve point limits are family-specific; copy verbatim from existing
confs when introducing new THERMALGROUP coefficients or FANDEFAULT_GOVERNOR values.
Troubleshooting
| Error | Cause | Solution |
|---|---|---|
| nvfancontrol.service fails to start after edit | FANDEFAULTPROFILE references a profile that was removed or renamed |
Set FANDEFAULTPROFILE to an existing FAN_PROFILE <name> in the same <FAN N>. |
| Fan thrashes near a curve point | HYST too low |
Raise HYST on the affected row (typical 2–5 °C). |
| Fan lags reality | HYST too high |
Lower HYST on the affected row. |
| Fan never spins up at high temp | Curve missing a row above GROUPMAXTEMP, or trailing 0 0 truncates above ambient |
Add a high-temp tuple with non-zero PWM/RPM; pin the over-temp row only above GROUPMAXTEMP. |
| Daemon parse error referencing column count | Curve row not 4-column TEMP HYST PWM RPM |
Restore 4-column shape; remove trailing whitespace and stray columns. |
RPM target never reached in close-loop |
Target above tach-reported max, or FANCONTROL set to openloop |
Lower RPM to within the fan's mechanical max, or switch FANDEFAULTCONTROL to close_loop. |
Change vanished after /jetson-init-image re-extract |
Edit landed in <bspimage.rootpath> instead of <source.root_path> overlay |
Re-apply via the [Overlay edit recipe](#overlay-edit-recipe-apply-before-any-operation) so the change is committed in the overlay tracker. |
| Sibling carrier boots with wrong tach ceiling after propagation | Blanket file copy clobbered carrier-specific THERMAL_GROUP / RPM rows |
Port only the changed lines per [Propagation set](#propagation-set--confs-to-keep-in-sync); restore the carrier's original THERMAL_GROUP. |
Deploy
The customization commit in the overlay tracker does not reach the device on its own. The Deploy chain:
/jetson-promote-image— copies every tracked file in the overlay
into <bspimage.rootpath>/LinuxforTegra/. Diff-aware (skip byte-identical); uses sudo cp -p for rootfs/* destinations.
/jetson-flash-image— flashes the updatedbsp_imageto the
device.
- (Alternate, no flash) Copy
<source.rootpath>/Linuxfor_Tegra/rootfs/etc/nvpower/nvfancontrol/<conf>
directly to the running target's /etc/nvpower/nvfancontrol/<conf>, then sudo systemctl restart nvfancontrol.service (or reboot).
Editing <source.rootpath>/... without committing — or editing <bspimage.root_path>/... directly — does nothing for /jetson-promote-image and is silently lost on the next /jetson-init-image re-extract.