System designTroubleshooting

SPI not working after a CSI patch on Jetson: restore the pinmux DTB

SPI not working after a Jetson CSI camera patch? The vendor DTB dropped the exp-header-pinmux node. Test it, merge the section back, keep the camera working.

LumaSpec Vision11 min read Share on LinkedIn

On this page 10 sections
  1. 01The symptom that names the fault
  2. 02Why a camera patch wipes the header pins
  3. 03Three checks before you change anything
  4. 04The fix: splice exp-header-pinmux back in
  5. 05Worked example: Orin Nano board, JetPack 5.1.3 with an L4T-35.2.1 CSI patch
  6. 06When the usual advice is wrong
  7. 07Prevention: own a golden tree
  8. 08An exit ramp: USB3 or GigE never touches the pinmux
  9. 09Checklist for the next patch
  10. 10Questions

You proved SPI0 and SPI1 on a Jetson Orin Nano header with a loopback wire and a protocol analyzer, then installed a camera vendor's CSI kernel patch to bring the sensor up. The camera streams. The SPI is dead: the spidev devices still appear, a transfer runs and returns without error, and the pins sit flat on the analyzer. That signature is a device-tree fault, not an SPI fault. The patch replaced the whole kernel DTB, the replacement carries no exp-header-pinmux section, and nothing maps the header pads to the SPI controllers any more. This guide runs the diagnosis in order of probability, shows the merge that restores the pins without losing the camera, and lists the habits that stop the next patch from repeating it.

The symptom that names the fault

After the patch you can still run sudo modprobe spidev and see /dev/spidev0.0, /dev/spidev0.1, /dev/spidev2.0 and /dev/spidev2.1. A transfer on SPI0 or SPI2 initiates and completes from the driver side. On the pins there is no clock, no chip-select and no data edge. The controller is present and the driver has bound to it; the pads are simply not connected to it.

On this board the SPI controller nodes ship with the SoC base tree, which the vendor patch kept. The header pinmux came from the configuration tool you ran before the patch, and its output lived in the DTB the patch overwrote. A clean transfer plus a flat SCK line is that exact split: controller alive, pads unmuxed. Work it as a pinmux fault first.

Why a camera patch wipes the header pins

When you enable SPI with jetson-io, the settings land in the booted device tree as a root-level node named exp-header-pinmux, holding the pad mux and pad control entries that put SPI0 and SPI1 on specific expansion-header pins. A CSI bring-up package for an out-of-tree driver typically ships a complete prebuilt kernel and DTB for one L4T release — here L4T-35.2.1-5.1.0 on a board running JetPack 5.1.3 — and its installer overwrites kernel_tegra234-p3767-0003-p3768-0000-a0.dtb with that file. The vendor tree has the camera nodes. It does not have your header edits, because those lived in the file just overwritten.

Two more things go with it: the jetson-io tool, which the patched boot flow no longer carries, and any UART or I2C you had enabled the same way. This sits in the same family as a kernel patch that cannot find its file because the source tree is wrong, and it shares a root cause with missing DTBs after a camera driver update: the tree on the board and the tree the package expects stop being the same object.

Three checks before you change anything

1. The booted tree, not the file on disk. List the live tree at /proc/device-tree/ and look for exp-header-pinmux. The failing case had a host-side DTB copy that looked correct beside a booted tree that lacked the node; a file comparison cannot settle this, only the running tree can. If /boot/extlinux/extlinux.conf lists several boot entries, the default entry decides which DTB loads, so first confirm you are booted into the image you patched.

2. The driver layer. Run sudo modprobe spidev, then list the /dev/spidev nodes. Nodes present mean the controller nodes exist in the running tree and the driver bound. That is necessary but says nothing about the pads.

3. The pads. Put the analyzer on SCK and chip-select while a transfer runs, and read the pad registers. In the failing case the SPI1 pad registers at 0x0243d008, 0x0243d018, 0x0243d028, 0x0243d038 and 0x0243d040 returned 59, 55, 1055, 59 and 55 — GPIO-style defaults, not the mux values of the boot where SPI had worked. Two registers at 59, two at 55 and one at 1055 across five addresses is a reset pattern, not a configuration someone programmed.

What you observe Layer at fault Fix
/dev/spidev nodes exist, transfer returns, SCK flat Header pinmux missing from the booted DTB Merge exp-header-pinmux back in
No /dev/spidev nodes after modprobe Controller nodes or driver build Fix the tree nodes or rebuild the module
Clock toggles, data wrong or partial Electrical: mode, speed, wiring Correct CPOL/CPHA, lower the clock rate, rewire
SPI works after the merge but the camera is gone Splice dropped the camera nodes Re-merge from a source tree of the same L4T release

The fix: splice exp-header-pinmux back in

The repair that closed this case: take the section from a tree where SPI provably worked, put it into the patched tree, recompile, reinstall.

  1. Build the known-good tree. Boot the stock image (here JetPack 5.1.3), enable SPI0 and SPI1 with jetson-io, and confirm with the loopback wire and the analyzer. Copy that DTB off the board — the exact file its boot entry loads.
  2. Decompile both trees. In the commands below, good-spi.dtb, good.dts and patched.dts are working names; kernel_tegra234-p3767-0003-p3768-0000-a0.dtb is the real file on this board.
dtc -I dtb -O dts -o good.dts good-spi.dtb
dtc -I dtb -O dts -o patched.dts kernel_tegra234-p3767-0003-p3768-0000-a0.dtb
  1. In good.dts find exp-header-pinmux { with grep -n exp-header-pinmux good.dts and copy the entire node, opening brace to matching close, sub-nodes and all.
  2. Paste it into patched.dts at the same level — it is a root-level node — and compile the result to a new file:
dtc -I dts -O dtb -o merged.dtb patched.dts
  1. Copy the installed DTB aside as a backup, copy merged.dtb over the installed file — the path the FDT line in /boot/extlinux/extlinux.conf names — and reboot.
  2. Run sudo modprobe spidev, repeat the analyzer test with the loopback wire from MOSI to MISO, and confirm the camera still enumerates and streams. If the camera side misbehaves instead, the IMX477 CSI bring-up checks walk that half.

If dtc reports unresolved labels or phandle errors when compiling, the copied section references something the vendor tree names differently. Source the section from a DTB built on the same L4T release as the patch, and the references line up.

Worked example: Orin Nano board, JetPack 5.1.3 with an L4T-35.2.1 CSI patch

Inputs, all from the case:

  • Board: Orin Nano developer board on stock JetPack 5.1.3, SPI0 and SPI1 enabled with jetson-io and confirmed electrically with spidev_test plus a protocol analyzer.
  • Change: CSI camera package built against L4T-35.2.1-5.1.0. The camera streams afterward; jetson-io is gone.
  • After the patch: sudo modprobe spidev yields /dev/spidev0.0, /dev/spidev0.1, /dev/spidev2.0 and /dev/spidev2.1; transfers initiate on SPI0 and SPI2 with zero activity on the analyzer.
  • Pad read-backs on the SPI1 pins: 0x0243d008 = 59, 0x0243d018 = 55, 0x0243d028 = 1055, 0x0243d038 = 59, 0x0243d040 = 55.

Rule applied: spidev nodes present, transfer returns, SCK flat — controller bound, pads unmuxed — the booted DTB lost the header pinmux.

Procedure and evidence: the stock-with-SPI decompile contained the exp-header-pinmux node and the patched decompile did not. One node present, one absent: that is the whole delta the fix addresses. The node was copied whole, the tree recompiled and reinstalled. Result: spidev transfers on SPI0 and SPI1 showed clock, chip-select and MOSI edges on the analyzer again, and the CSI camera kept streaming from the same boot.

Decision: after any merge, rerun the same transfer at the analyzer. Clock and chip-select toggling, with loopback bytes in equal to bytes out, closes the job. If the pins are still flat, the booted tree is not the file you merged — go back to check 1 and read /proc/device-tree again.

When the usual advice is wrong

  • Run jetson-io again. The tool left with the patched boot flow, and where it does run it edits a stock-based tree and adds a boot entry for it. Boot that entry and you trade the camera for the SPI.
  • The DTB on disk matches my host copy, so the tree is fine. The host-side DTB from an L4T package is the stock base tree; it also lacks exp-header-pinmux, so the comparison looked clean in this case and proved nothing about what booted.
  • /dev/spidev0.0 exists, so SPI works. The node proves the driver bound to a controller node. The pads are decided elsewhere in the tree.
  • Reflash stock and reapply everything. It works, and it throws away a working camera configuration. The merge keeps both in one boot.

Prevention: own a golden tree

Before any vendor package lands, copy the DTB the boot entry loads together with that entry in /boot/extlinux/extlinux.conf, note the L4T version the package targets, and record a set of good pad read-backs. Ask the camera vendor for a device-tree overlay rather than a full DTB: an overlay adds camera nodes without touching your pinmux, which is exactly the property that failed here — CSI mapping in a device tree overlay shows how that mechanism is meant to work. Keep the good and merged .dts files under version control. Release mismatch is the same trap when a uvcvideo patch meets the wrong kernel source: package and board must agree on the release before anything else.

An exit ramp: USB3 or GigE never touches the pinmux

If the sensor is not yet locked in, the cleanest fix is not to fight the device tree at all. USB3 and GigE cameras enumerate from a stock kernel: no out-of-tree driver, no DTB replacement, and the SPI header stays exactly as jetson-io left it. Board-level cameras fit the same embedded enclosures a CSI module does, and the board-level cameras range runs from compact M12-mount modules to C-mount parts.

Board-level camerasHikrobot MV-CB016-10GM-M-W 1.6 MP GigE board-level camera, IMX296, Mono, M12 lens mount, WTB GigE portMV-CB016-10GM-M-W
Sensor format
1/2.9"
Lens mount
M12
$559.00View part
Board-level cameras3.1 MP USB3 board-level camera, IMX252, 1/1.8" global shutter, 120 fps (LS-BCM-0003)LS-BCM-0003
Sensor format
1/1.8"
Lens mount
C-mount
$690.00View part

The MV-CB016-10GM-M-W is a 1.6 MP GigE board-level camera with an IMX296 sensor and an M12 lens mount. The LS-BCM-0003 is a 3.1 MP USB3 board-level camera with an IMX252 sensor, a 1/1.8" global shutter running at 120 fps, and a C-mount. On the USB3 side the MV-CB060-10UM-B is a 6 MP board camera with a 1/1.8" IMX178 sensor, delivered as a bare board.

Board-level camerasHikrobot MV-CB060-10UM-B 6 MP Board Level Camera, USB3, IMX178, Mono, Bare Board, V4MV-CB060-10UM-B
Sensor format
1/1.8"
Interface
USB3
Resolution
6-10 MP
Shutter
Rolling
$239.00View partBuild a system with it

When you pair a USB3 module with a carrier, match the board-level camera and USB3 bridge before you stream. For wiring, 3 m of high-flex USB3 camera cable covers a drag-chain run from camera to enclosure feed-through, and the GigE camera cables include the LS-CBL-0199, a 3 m high-flex cable with screw locks. Mounting does not change with the interface: camera stands and brackets position a board camera like any other. Browse both interface families in the camera finder or the accessories finder.

Cables3 m USB3.0 camera cable, high-flex (drag chain) (LS-CBL-0219)LS-CBL-0219$62.00View partBuild a system with it

Checklist for the next patch

Before the patch:

  • Copy the DTB the boot entry loads, and the extlinux.conf entry itself.
  • Note the L4T release the package targets, and record good pad read-backs.

During the merge:

  • Decompile the installed DTB before and after; keep both .dts files.
  • Copy exp-header-pinmux whole, braces to braces.

After the merge:

  • Confirm exp-header-pinmux in /proc/device-tree, then the /dev/spidev nodes.
  • Loopback test: MOSI to MISO, bytes out equal bytes in.
  • Analyzer: clock and chip-select toggle during a transfer.
  • Camera enumerates and streams from the same boot.

If your patch set-up or camera choice needs a second opinion, the configurator walks the interface decision, or send the boot log and both .dts files to us through the contact page.

Questions

Why do the /dev/spidev nodes exist if the pins are dead?

The node appears when the spidev driver binds to an SPI controller node in the booted tree, so it proves the controller exists and the driver loaded, nothing more. Which physical pad carries SCK, MOSI, MISO and chip-select is set by the pinmux elsewhere in the same tree. With the pad left in its default function the transfer still executes inside the controller, returns cleanly, and never reaches the header. Treat a clean return with a flat clock line as a pinmux fault.

Where did jetson-io go after the camera patch?

The tool edits the device tree files of the stock boot flow. A vendor CSI package that installs its own kernel and prebuilt DTB replaces those artifacts, so the tool stops being available, and the SPI settings it had written went into the old DTB, which the same install overwrote. That is why the header worked before the patch and died after it with no wiring change at all.

Can I take the exp-header-pinmux section from the DTB on my host PC?

Only if that file came from a boot where SPI actually worked. The DTB in an L4T release package is the stock base tree, with no header edits in it, which is why comparing the patched tree against a host copy in this case looked correct and proved nothing. Source the section from your pre-patch backup, or boot the stock image, enable SPI, and copy the tree off that running board.

What is inside the exp-header-pinmux section?

A root-level node in the kernel device tree holding the header configuration the tool wrote: pad mux and pad control entries that place SPI0, SPI1 and anything else you enabled onto specific expansion-header pins. Copy it whole, opening brace to matching close, sub-nodes included. A partial copy leaves some pads muxed wrong, and you end up chasing single pins instead of fixing the tree once.

Will the merged tree survive the next JetPack upgrade?

No. An upgrade replaces the kernel and DTB again, so plan to redo the merge from a new known-good tree each time the base release changes. Keep the good and merged .dts files under version control so the splice is a two-minute job, and push the camera vendor for a device-tree overlay, which adds its nodes without touching your pinmux at all.