March 13, 2026 · Alex Potanin

How to Get sel4test Running on NVIDIA Jetson Orin Nano 8GB

This was an experiment with a lot of dead ends described at the end of the article. The final port is not very large but it took a lot of false leads by Claude Code Opus 4.6 Max to get there. From this you can decide if it is worth using Opus 4.6 or not - other models didn't even get anywhere, so the reason for this post as that Opus 4.6 got at least somewhere. My lesson is that Opus 4.6 is really not able to grasp architectural issues and produces solutions too complex for their own good and then blames others (like nVidia etc) rather than admitting that it made a wrong choice. Opus 4.6 also seems to take as gospel anything that it observed earlier without being able to use common sense to dismiss it as irrelevant as the problem is solved in other ways. Please contact me if you are interested to find out more. Thanks, Alex.

A complete step-by-step guide for porting the seL4 microkernel to the NVIDIA Jetson Orin Nano 8GB Developer Kit. Covers hardware setup, serial console wiring, cross-compiling sel4test, and booting from UEFI. All 141 compiled tests pass deterministically with stock NVIDIA firmware.

141 / 141 tests pass · 42 disabled · 0 failures

Contents

  1. Hardware
  2. Serial Console
  3. Jetson Linux
  4. Building seL4
  5. Booting
  6. Technical Details
  7. Repository Structure
  8. Dead Ends

The port changes 3 seL4 repositories (github.com/potanin, orin-nano branch). Stock NVIDIA firmware works unmodified. sel4test and seL4_libs run unmodified from upstream.

1. Hardware

ItemNotes
Jetson Orin Nano 8GB Developer KitThe carrier board with the Orin Nano module. Other Orin variants may work but are untested.
USB-UART adapter (3.3V)CP2102, FTDI FT232R, or CH340. Must be 3.3V logic — 5V will damage the Jetson.
MicroSD card (32GB+)For JetPack Linux installation and as the UEFI boot medium.
Ethernet cableConnects the Jetson to your LAN for HTTP boot.
DisplayPort monitor + USB keyboardFor initial UEFI configuration. Not needed after one-time setup.
DC power supply5V/4A barrel jack (included with Dev Kit), or USB-C PD.
Host machineLinux x86_64 (Ubuntu 22.04 recommended). For cross-compiling seL4.
Dupont jumper wires3 female-to-female wires for the serial console.

2. Serial Console

The serial console uses UARTC on the J14 button header — the small header near the three buttons on the carrier board (not the 40-pin GPIO header).

J14 PinSignalWire ColorConnect to Adapter
Pin 3RXDGreenAdapter TX
Pin 4TXDWhiteAdapter RX
Pin 7GNDBlackAdapter GND
J14 Button Header (looking at the board, buttons nearest to you):

    Pin 1   Pin 2
    Pin 3   Pin 4    <- RXD (3), TXD (4)
    Pin 5   Pin 6
    Pin 7   Pin 8    <- GND (7)
    Pin 9   Pin 10
    Pin 11  Pin 12

    DO NOT connect VCC/3.3V from the adapter to the Jetson.

Serial parameters: 115200 baud, 8N1. Connect with:

picocom -b 115200 /dev/ttyUSB0

(Your device may be /dev/ttyUSB0, /dev/ttyUSB1, or /dev/ttyACM0. Use dmesg | tail after plugging in the adapter.)

3. Jetson Linux

Before building seL4 you need JetPack 6.x on the Jetson. Follow NVIDIA's Getting Started guide to flash your Dev Kit. Stock firmware works unmodified.

Once Linux is running, extract the hardware device tree and other reference information used to create the seL4 platform port:

# Copy the device tree blob from the running Jetson
sudo cat /sys/firmware/fdt > tegra234-orin-nano.dtb

# Decompile to human-readable DTS (on host or Jetson)
dtc -I dtb -O dts -o tegra234-orin-nano.dts tegra234-orin-nano.dtb

# Save the kernel config (useful reference for clocks, drivers, etc.)
zcat /proc/config.gz > kernel-config

# Dump the memory map
cat /proc/iomem > iomem.txt

The DTS in the seL4 kernel repo (tools/dts/orin-nano.dts, 11,514 lines) is the verbatim output of this extraction. The seL4 overlay (overlay-orin-nano.dts) selects the relevant nodes — UART, GIC, timer, and memory regions — from this full device tree.

Key information extracted from Linux:

4. Building seL4

Install build dependencies per the seL4 host dependencies guide, including the AArch64 cross-compiler (gcc-aarch64-linux-gnu).

Clone Repositories

3 forked repos (github.com/potanin, orin-nano branch) plus upstream dependencies:

mkdir -p ~/jetson/potanin-git && cd ~/jetson/potanin-git

# Forked repos (github.com/potanin, branch: orin-nano)
git clone -b orin-nano https://github.com/potanin/seL4.git kernel
mkdir -p tools
git clone -b orin-nano https://github.com/potanin/seL4_tools.git tools/seL4
mkdir -p projects
git clone -b orin-nano https://github.com/potanin/util_libs.git projects/util_libs

# Upstream repos (pinned commits)
git clone https://github.com/seL4/seL4_libs.git projects/seL4_libs
git clone https://github.com/seL4/sel4test.git projects/sel4test
git clone https://github.com/seL4/musllibc.git projects/musllibc
git clone https://github.com/seL4/sel4_projects_libs.git projects/sel4_projects_libs
git clone https://github.com/seL4/sel4runtime.git projects/sel4runtime
git clone https://github.com/nanopb/nanopb.git tools/nanopb
git clone https://github.com/riscv/opensbi.git tools/opensbi

# Required build symlinks
ln -sf projects/sel4test/easy-settings.cmake easy-settings.cmake
ln -sf tools/seL4/cmake-tool/init-build.sh init-build.sh
ln -sf tools/seL4/cmake-tool/griddle griddle

Build

cd ~/jetson/potanin-git
mkdir build-orin && cd build-orin

../init-build.sh \
    -DPLATFORM=orin-nano \
    -DAARCH64=TRUE \
    -DARM_HYP=ON \
    -DSIMULATION=FALSE \
    -DRELEASE=FALSE

ninja

Output: build-orin/images/sel4test-driver-image-arm-orin-nano — a PE/COFF EFI executable (~6 MB).

Clean rebuild required if you change the device tree overlay: rm -rf build-orin and re-run init-build.sh + ninja.

5. Booting

Option A: HTTP Boot (Recommended)

HTTP boot lets you rebuild on the host and immediately reboot the Jetson without touching the SD card.

# Serve the EFI binary from the host machine
mkdir -p /srv/tftp
cp build-orin/images/sel4test-driver-image-arm-orin-nano /srv/tftp/sel4.efi
cd /srv/tftp && python3 -m http.server 8080

One-time UEFI setup: Connect DisplayPort + keyboard. Press ESC during boot to enter UEFI Setup. Add an HTTP boot entry pointing to http://<host-ip>:8080/sel4.efi. Set it as the first boot option.

Option B: SD Card Boot

# Format SD card as FAT32, then:
sudo mount /dev/sdX1 /mnt/sd
sudo mkdir -p /mnt/sd/EFI/BOOT
sudo cp build-orin/images/sel4test-driver-image-arm-orin-nano \
        /mnt/sd/EFI/BOOT/BOOTAA64.EFI
sudo umount /mnt/sd

Expected Output

The elfloader runs silently — UEFI's page tables do not map the UART MMIO region, so there is no serial output until the kernel initializes its own UART driver:

Booting all finished, dropped to user space
Starting test suite sel4test
Starting test 0: Test that there are tests
Starting test 1: SYSCALL0000
...
Test suite passed. 141 tests passed. 42 tests disabled.
All is well in the universe

6. Technical Details

UEFI boots to EL2 (not EL1), so seL4 must build with ARM_HYP=ON. The T234 has 6 Cortex-A78AE cores (ARMv8.2-A); seL4 runs single-core (non-SMP).

Memory Map

RegionStartEndSize
Region 10x800000000xbe000000992 MB
Gap (CO:43)0xbe0000000xc200000064 MB
Region 20xc20000000x100000000992 MB

The gap at 0xbe000000 is firmware carve-out CO:43 (allocated by MB1, hardware-protected by the SNOC bus fabric). Any CPU access triggers an uncorrectable RAS error. Physical addresses 0x0–0x200000 are also reserved to prevent device untypeds covering firmware-protected addresses.

Fixes That Should Go Upstream

Several of our changes fix issues that affect other platforms, not just the T234. First the two issues that seem real and need to be fixed upstream:

Now the issues suggested by Claude Opus 4.6 that may not be necessary as it may not grasp the architectural details and the actual root causes of the problems:

7. Repository Structure

kernel/ — potanin/seL4 (12 files)

FileChange
src/plat/orin-nano/config.cmakeNew platform declaration: Cortex-A78AE (A72 proxy), GICv3, 40-bit PA override
src/plat/orin-nano/overlay-orin-nano.dtsDevice tree overlay: UARTC, dual memory regions with 64 MB carve-out gap
tools/dts/orin-nano.dtsFull device tree extracted from running Linux on the hardware
tools/hardware.ymlRegister orin-nano platform in seL4 hardware database
libsel4/sel4_plat_include/orin-nano/sel4/plat/api/constants.hPlatform constants header (required by build system)
src/drivers/serial/tegra_omap3_dwapb.c8250-style UART driver shared with other Tegra/OMAP/DW-APB platforms
src/drivers/serial/config.cmakeWire orin-nano to the tegra_omap3_dwapb serial driver
src/arch/arm/64/kernel/vspace.cMap only SDRAM regions in kernel window instead of full PA range; prevents speculative RAS errors
src/arch/arm/kernel/boot.cReserve physical addresses 0x0–0x200000 to prevent device untypeds covering firmware-protected memory
include/arch/arm/arch/machine.hUse cleanCacheRange_RAM (dc cvac to PoC) for page table cleaning; PoU does not reach T234 SLC
src/arch/arm/64/object/objecttype.cSame PoU→PoC cache fix for page table object creation
src/arch/arm/config.cmakePA size override mechanism; fix S2_START_L1 cmake variable name bug

tools/seL4/ — potanin/seL4_tools (8 files)

FileChange
elfloader-tool/src/arch-arm/sys_boot.cFlush page tables to DRAM with dc cvac (bypasses SLC); reordered boot flow for T234 cache maintenance
elfloader-tool/src/arch-arm/armv/armv8-a/64/mmu-hyp.SRewrite cache disable to skip set/way flush (SLC unreachable); register-only MMU transition
elfloader-tool/src/arch-arm/64/mmu.cIdentity-map full 4 GiB as MT_NORMAL 1 GiB PUD blocks (prevents speculative translation faults on T234)
elfloader-tool/src/binaries/efi/efi_init.cNo-op plat_console_putchar override (UEFI doesn't map UART MMIO at 0x0c280000)
elfloader-tool/include/arch-arm/64/mode/assembler.hFix CCSIDR parsing for FEAT_CCIDX (A78AE uses 64-bit format, not 32-bit)
elfloader-tool/src/drivers/uart/8250-uart.cAdd nvidia,tegra194-hsuart compatible string
elfloader-tool/src/binaries/efi/gnuefi/elf_aarch64_efi.ldsAdd _end symbol required by EFI loader
cmake-tool/helpers/application_settings.cmakeAdd orin-nano to EFI boot platform list

projects/util_libs/ — potanin/util_libs (5 files)

FileChange
libplatsupport/plat_include/orin-nano/platsupport/plat/clock.hPlatform stub header (required by build system)
libplatsupport/plat_include/orin-nano/platsupport/plat/i2c.hPlatform stub header (required by build system)
libplatsupport/plat_include/orin-nano/platsupport/plat/serial.hPlatform serial configuration: UARTC address and parameters
libplatsupport/plat_include/orin-nano/platsupport/plat/timer.hPlatform timer configuration: NV timer addresses and interrupt routing
libplatsupport/src/mach/nvidia/timer.cEnable timer interrupt routing for orin-nano (same as TX2)

8. Dead Ends

The port took roughly 120 iterations over 15 phases. Many of those iterations explored approaches that turned out to be wrong. Documenting them here so others can avoid the same traps.

Wrong UART: UARTA and the BPMP keepalive saga

The biggest time sink. We initially used UARTA at 0x03100000 (40-pin GPIO header), which required a custom pinmux overlay flashed into MB1. It worked at first, but the UARTA clock died after 5–10 minutes — BPMP power management aggressively gates clocks when no Linux driver holds a reference.

This led to building an entire BPMP IPC keepalive subsystem inside the seL4 kernel: IVC (Inter-VM Communication) protocol, HSP doorbell interrupts, MRQ_CLK messages, timer-tick integration. Over 40 versions (v70–v112) were spent tuning this. Results were non-deterministic (25 to 122 tests per boot depending on BPMP firmware state). Calling BPMP IPC from the timer interrupt blocked the kernel for ~10 ms and violated real-time scheduling guarantees. A fire-and-forget variant corrupted the IVC TX/RX counters.

The fix was trivial: switch to UARTC at 0x0c280000 on the J14 button header. UARTC is in the Always-On (AON) power domain and stays alive without any keepalive. All BPMP kernel code was deleted.

TCU (Tegra Combined UART) via USB

Before physical UART worked, we tried using the TCU — the Orin Nano’s default console, which operates as a USB gadget through the XUSB controller. After ExitBootServices the USB stack is gone, so no USB device appears on the host. Dead end.

Cross-boot diagnostics via SRAM and IVC shared memory

With no serial output after ExitBootServices, we tried writing diagnostic values to persistent memory (SRAM at 0x40010000, IVC shared memory regions) and reading them back on the next boot. SRAM wasn’t mapped in UEFI (crashed on access). IVC regions were cleared each boot. EFI SetVariable didn’t work after our MMU changes. Each attempt required two power cycles and none persisted reliably.

Garbled serial blamed on pinmux

When garbled bytes appeared on UARTA, investigation focused on pinmux configuration. A GPIO bit-bang test toggled pins directly — no output, because PADCTL is firewalled on T234 (CPU cannot read/write pinmux registers). The actual cause was a baud rate mismatch: UEFI released the UART clock reference at ExitBootServices, changing the effective clock rate. Fix: read UEFI’s divisor registers before ExitBootServices and re-send CLK_ENABLE afterward.

CBB ERD and IOB/ACI ERRCTLR — masking vs fixing RAS

We tried masking RAS errors at the bus fabric level. Setting the CBB (Control Backbone) Error Response Disable register at 0x13a3a004 converted bus errors into benign responses. But the IOB (I/O Bridge) generates its own RAS error independently of CBB. The error path is: SLC eviction → writeback → SCF → IOB → RAS. CBB is downstream of where the IOB error fires.

Writing directly to IOB/ACI ERRCTLR registers failed too — they are Secure-only. Writes from NS-EL2 are silently ignored.

SDEI handlers to catch RAS

Registered SDEI (Software Delegated Exception Interface) handlers for all 13 RAS events. The handlers were called successfully. But ATF unconditionally powers off the faulting core after dispatching the SDEI event, regardless of the handler’s return value. The core was killed anyway.

Custom BL31 — unnecessary in the end

Built custom NVIDIA ATF from source, modifying tegra234_ras_handler() to log errors instead of killing the core. Required a complex QSPI flashing journey (USB Recovery Mode, A/B boot slots, TOS image packing). Achieved deterministic 141/141 pass — but treated the symptom (core kill) rather than the cause (speculative accesses to protected memory).

Three software-only fixes later eliminated the RAS errors entirely: MT_NORMAL identity mapping in the elfloader, SDRAM-only kernel window, and 2 MiB low-address reservation. Stock BL31 restored.

XN-only (execute-never) mapping

Mapped the full PA range with UXN=1 instead of leaving non-DRAM unmapped. XN prevents speculative instruction fetches but not speculative data reads. The A78AE still speculatively reads data from Normal-mapped addresses. RAS errors persisted. Only leaving non-DRAM regions completely unmapped works.

PE/COFF sensitivity misdiagnosis

Removing UEFI ConOut code from efi_init.c caused a Synchronous Exception. We initially blamed NVIDIA’s PE/COFF loader being sensitive to binary layout changes. The actual cause: the default weak plat_console_putchar calls uart_8250_putchar(), which writes to UART MMIO at 0x0c280000. UEFI doesn’t map that address → data abort. Fix: override with a no-op (7 lines).

Implementation-defined CPU registers

Attempted to modify speculative behavior via ACTLR_EL2 and other implementation-defined registers. On the A78AE (r0p1), ATF traps and silently discards all EL2 writes to these registers. No CPU-level workarounds are possible from EL2.

MCE ROC_FLUSH_CACHE SMC calls

Tried T186-era MCE commands (ROC_FLUSH_CACHE, ROC_CLEAN_CACHE) via SMC to flush the SLC. Not supported on T234 — both return SMC_UNK. The SLC cannot be flushed from NS-EL2 on T234.

dc ivac to discard dirty SLC lines

Tried dc ivac (invalidate without clean) to discard dirty cache lines at protected addresses without triggering a writeback. The A78AE implementation cleans dirty lines before invalidating — dc ivac triggers the same writeback as dc civac. IOB RAS fires, core killed.

QSPI flash from the running system

During the custom BL31 phase, we tried every approach to write QSPI from a running OS: Linux MTD, /dev/mem, UEFI Shell mm, UEFI FirmwareManagement Protocol, NorFlashDxe, OP-TEE TA. All failed because the T234 CBB firewall blocks all Non-Secure CPU access to the QSPI controller at 0x03270000. Only USB Recovery Mode works (BROM exposes USB, MB1/MB2 have Secure access to QSPI).

BPMP IPC for UARTC clock enable

After switching to UARTC, we added BPMP IPC code to the elfloader to explicitly enable the UARTC clock (CLK_ENABLE + SET_RATE + RESET_DEASSERT) before ExitBootServices. ~60 lines of shared-memory IPC and doorbell code. Turned out to be completely redundant — firmware (MB1/MB2/BPMP/SPE) already enables UARTC from power-on (serial output appears at timestamp 0000.066, long before our EFI application loads). Removed.

Join the discussion

Have questions or feedback? Comment on this post on LinkedIn.