Skip to main content
  1. Posts/

Orange Pi 5 Plus: Diagnosing Hard Resets and Setting Up Remote Access

The Board #

An Orange Pi 5 Plus running Ubuntu 26.04 LTS (aarch64). The RK3588 SoC has 4× Cortex-A55 little cores and 4× Cortex-A76 big cores (up to 2.4 GHz), 16 GB RAM, and a Mali-G610 GPU. USB-C only for power — no barrel jack.

This board is used as an arcade/retro gaming cabinet. The emulation setup is covered in a separate post; this one covers the infrastructure: getting the board stable and accessible.


Chapter 1: The Mystery Hard Resets #

Shortly after setup the board started hard-resetting under load — no kernel panic, no OOM log, no graceful shutdown. The PMIC was simply cutting power.

Reproducing the pattern #

Stress tests with stress-ng quickly mapped the boundary:

TestResult
CPU + VM 20% load, no freq capCrash in ~30 seconds
CPU-only 20% load10 minutes stable
CPU-only 80% load, no freq capCrash
CPU + VM 100% load, cap 1.608 GHzStable
CPU + VM 100% load, cap 1.8 GHzStable
CPU + VM 100% load, cap 2.0 GHzCrash ~2.5 min

The memory bandwidth worker (--vm) was the trigger. Adding DRAM pressure caused resets even at 20% CPU utilisation — pure CPU work was far more forgiving.

Root cause: underpowered supply #

The Orange Pi 5 Plus requires 5 V / 4 A (20 W) under load. Importantly, the board does not implement USB Power Delivery — no PD negotiation happens regardless of what charger you plug in. The TCPM node at /sys/class/power_supply/tcpm-source-psy-6-0022/ always reports online=0 and that is normal for this board, not a fault. PD capability of the charger is irrelevant.

The original supply simply could not deliver 4 A sustained. It supplied enough current at idle but the moment DRAM and CPU demanded full power simultaneously the voltage sagged, the PMIC hit undervoltage protection, and the board cut power instantly.

The workaround (historical) #

While waiting for a replacement, a systemd service (limit-cpufreq.service) capped the A76 big cores at 1.608 GHz and forced schedutil. The board ran stable at that ceiling indefinitely but lost roughly 3× emulation throughput.

The fix #

Replace the supply with one rated for at least 5 V / 4 A. PD support is irrelevant. After the swap:

  • Full CPU + VM stress at 2.4 GHz unrestricted passes 5 minutes clean.
  • VM throughput: ~19 000 → ~72 000 bogo ops/s (3.7×).
  • limit-cpufreq.service disabled.

Chapter 2: Governor Tuning #

With a healthy supply there is no reason to throttle. Both CPU and GPU are locked to performance via a systemd service (gpu-governor.service).

  • CPU: All 8 cores at max frequency (A55 up to 1.8 GHz, A76 up to 2.4 GHz).
  • GPU: Mali-G610 at 1000 MHz (top of the 300–1000 MHz range).

performance eliminates ramp-up latency entirely. For a cabinet that is either idle at a menu or running a fixed-framerate emulator, the transient response of schedutil adds jitter with no benefit.

To revert to sensible non-capped defaults:

for gov in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
    echo schedutil > "$gov"
done
echo simple_ondemand > /sys/class/devfreq/fb000000.gpu/governor
sudo systemctl disable --now gpu-governor.service

Chapter 3: Remote Access — VNC over SSH #

The cabinet runs headless most of the time. Remote access is via TigerVNC tunnelled through SSH so nothing is exposed on the network.

Server setup #

TigerVNC runs as the local user on display :2 (port 5902), bound to localhost only. The systemd service (tigervncserver@:2.service) starts on boot and restarts automatically on crash:

[Service]
Restart=always
RestartSec=3

The KDE xstartup disables the compositor (compositing effects are invisible over VNC and just add overhead) and runs autocutsel twice for bidirectional clipboard sync:

#!/bin/bash
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
/usr/bin/dbus-launch --exit-with-session startplasma-x11 &
autocutsel -fork
autocutsel -selection PRIMARY -fork

Connecting from Windows #

A PowerShell script (vnc-connect.ps1) handles the full connection in one double-click: SSH tunnel, audio streaming, VNC launch, and auto-exit when the remote session drops.

# Replace these three variables for your setup
$SBC       = "NNN.NNN.NNN.NNN"  # your SBC's IP address
$USER      = "your-username"    # SSH username on the SBC
$PORT      = 5902
$PASSWD    = "$env:USERPROFILE\.vnc-passwd"
$SCRIPTDIR = Split-Path -Parent $MyInvocation.MyCommand.Definition
$PADIR     = "$SCRIPTDIR\pulseaudio\bin"
$PAEXE     = "$PADIR\pulseaudio.exe"
$PACATEXE  = "$PADIR\pacat.exe"

# Start PulseAudio if not already running — kept alive between sessions
if (Test-Path $PAEXE) {
    if (-not (Get-Process pulseaudio -ErrorAction SilentlyContinue)) {
        Start-Process $PAEXE -ArgumentList "--daemonize=no" `
            -WorkingDirectory $PADIR -WindowStyle Hidden
        Start-Sleep -Seconds 3
    }
}

# SSH tunnel for VNC
$tunnel = Start-Process ssh `
    -ArgumentList "-4 -L ${PORT}:localhost:${PORT} -N $USER@$SBC" `
    -WindowStyle Hidden -PassThru

# Poll until VNC port is reachable
$elapsed = 0
while ($elapsed -lt 15) {
    $test = Test-NetConnection -ComputerName localhost -Port $PORT -WarningAction SilentlyContinue
    if ($test.TcpTestSucceeded) { break }
    Start-Sleep -Seconds 1; $elapsed++
}

# Stream raw PCM audio from SBC via SSH pipe → pacat.exe → Windows speakers
$audioStream = $null
if (Test-Path $PACATEXE) {
    $audioBat = "$env:TEMP\vnc-audio.bat"
    "@echo off`ncd /d `"$PADIR`"`nssh -4 -o IPQoS=lowdelay -o Compression=no $USER@$SBC " +
    "`"PULSE_RUNTIME_PATH=/run/user/1000/pulse parec --raw --format=s16le --rate=48000 " +
    "--channels=2 --latency-msec=50 -d auto_null.monitor`" | " +
    "pacat.exe --raw --format=s16le --rate=48000 --channels=2 --latency-msec=100 -p" `
    | Set-Content $audioBat
    $audioStream = Start-Process "cmd.exe" -ArgumentList "/c `"$audioBat`"" `
        -WindowStyle Hidden -PassThru
}

$vnc = Start-Process vncviewer `
    -ArgumentList "-FullScreen -RemoteResize -passwd `"$PASSWD`" localhost:$PORT" -PassThru

# Watch for TCP session drop — auto-kill vncviewer to avoid stuck "connection closed" dialog
$wasConnected = $false
while (-not $vnc.HasExited) {
    Start-Sleep -Seconds 2
    $conns = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties().GetActiveTcpConnections()
    $isConnected = [bool]($conns | Where-Object {
        $_.RemoteEndPoint.Port -eq $PORT -and
        $_.State -eq [System.Net.NetworkInformation.TcpState]::Established
    })
    if (-not $wasConnected -and $isConnected) { $wasConnected = $true }
    if ($wasConnected -and -not $isConnected) {
        Start-Sleep -Seconds 2
        $vnc | Stop-Process -Force -ErrorAction SilentlyContinue
        break
    }
}

# Cleanup — /T kills cmd.exe AND its child SSH+pacat processes
Stop-Process -Id $tunnel.Id -Force -ErrorAction SilentlyContinue
if ($audioStream) { & taskkill /F /T /PID $audioStream.Id 2>&1 | Out-Null }
Remove-Item "$env:TEMP\vnc-audio.bat" -Force -ErrorAction SilentlyContinue
# pulseaudio.exe intentionally left running for next session

Key details:

  • -4 forces IPv4 — Windows sometimes resolves the hostname to IPv6, silently breaking the tunnel
  • Port polling replaces a fixed sleep; fast reconnects work without a race condition
  • -RemoteResize matches the VNC session to whichever monitor it opens on
  • Audio streams raw PCM over a second SSH connection (see Chapter 4)
  • pulseaudio.exe is deliberately not killed on exit — see Chapter 4
  • The TCP watcher auto-exits without the “connection closed” dialog; closing vncviewer manually also exits cleanly since HasExited fires

Run without a terminal window via a shortcut:

powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File "vnc-connect.ps1"

PolicyKit popup suppression #

KDE shows a polkit popup on every VNC login complaining that network connections are blocked for non-local sessions. Silenced with a polkit rule at /etc/polkit-1/rules.d/50-vnc-netmanager.rules granting the local user silent permission for NetworkManager actions.


Chapter 4: Audio Streaming over SSH #

VNC has no audio channel. The solution is a second SSH connection carrying raw PCM from the SBC’s PulseAudio stack directly into Windows’ audio system — no network ports, no PulseAudio protocol, no firewall rules beyond the existing SSH tunnel.

The problem #

The cabinet alternates between two use modes:

  • Local (HDMI + TV): sound should come out the HDMI output on the SBC.
  • Remote (VNC): sound should come out Windows speakers.

A simple fixed sink doesn’t work. The sink must switch automatically when a VNC session connects or disconnects.

Virtual null sink #

PulseAudio’s module-null-sink creates a virtual audio device (auto_null) that always exists regardless of what physical hardware is present. Any application writing to it can be monitored and captured. This becomes the “VNC audio output” — applications play to auto_null and the SSH pipe reads from auto_null.monitor.

The switch daemon #

~/vnc-audio-switch.sh runs as a KDE autostart daemon and polls ss -tn every 3 seconds for an established TCP connection on port 5902:

#!/bin/bash
# Replace /run/user/1000/pulse with your user's runtime path (id -u gives the UID)
export PULSE_RUNTIME_PATH=/run/user/1000/pulse

pgrep -f "vnc-audio-switch.sh" | grep -v "^$$" | xargs kill 2>/dev/null
sleep 1

CONNECTED=false

move_streams_to() {
    local sink="$1"
    pactl list sink-inputs short 2>/dev/null | awk '{print $1}' | while read -r id; do
        pactl move-sink-input "$id" "$sink" 2>/dev/null
    done
}

while true; do
    if ss -tn | grep -q "ESTAB.*5902"; then
        if [ "$CONNECTED" = false ]; then
            CONNECTED=true
            pactl set-default-sink auto_null 2>/dev/null
            move_streams_to auto_null
        fi
    else
        if [ "$CONNECTED" = true ]; then
            CONNECTED=false
            PHYSICAL=$(pactl list sinks short 2>/dev/null \
                | grep -v auto_null | awk '{print $2}' | head -1)
            if [ -n "$PHYSICAL" ]; then
                pactl set-default-sink "$PHYSICAL" 2>/dev/null
                move_streams_to "$PHYSICAL"
            fi
        fi
    fi
    sleep 3
done

Two things happen on each transition:

  1. pactl set-default-sink changes where new streams go.
  2. move_streams_to explicitly moves already-playing streams — games and the ES-DE UI that were running before the connection state changed.

The SSH pipe #

parec reads the null sink’s monitor on the SBC and writes raw 16-bit stereo PCM to stdout. The SSH pipe carries it to Windows where pacat.exe feeds it to PulseAudio:

parec --raw --format=s16le --rate=48000 --channels=2 --latency-msec=50 \
    -d auto_null.monitor  →  SSH  →  pacat.exe --raw ... --latency-msec=100

SSH flags for low latency:

-o IPQoS=lowdelay -o Compression=no

Compression=no matters: default SSH compression adds variable latency that shows up as audio glitches under emulator load.

Windows PulseAudio #

The pgaskin Windows PulseAudio build (v15) runs as a background process and must be kept alive between sessions — not killed at VNC exit. Restarting it causes silent waveout reinit failures and leaves behind a stale PID file that prevents the next startup. vnc-connect.ps1 only starts it when not already running and never kills it on cleanup.

PipeWire TCP conflict #

Ubuntu 26.04 ships with PipeWire as the default audio server. PipeWire-Pulse can expose a TCP socket that conflicts with the raw SSH audio approach. That TCP module must be disabled in ~/.config/pipewire/pipewire-pulse.conf so the two don’t fight over the PulseAudio protocol port.

End result #

When vnc-connect.ps1 opens:

  1. SSH tunnel opens (VNC on port 5902).
  2. Audio SSH pipe starts — parec on SBC streams to pacat.exe on Windows.
  3. vnc-audio-switch.sh detects the new connection and switches the default sink to auto_null, moving any already-playing streams.
  4. VNC session opens with audio already playing through Windows speakers.

When the session closes:

  1. VNC TCP session drops; vnc-connect.ps1 auto-exits.
  2. Audio SSH pipe is killed (taskkill /T on the cmd.exe wrapper).
  3. vnc-audio-switch.sh detects port 5902 gone and switches back to the physical HDMI sink.
  4. Audio resumes on the TV.

Summary #

LayerChangeEffect
Power supplyReplaced underpowered supply with 5 V / 4 A unitBoard stable; 3.7× stress throughput vs. capped workaround
CPU governorschedutilperformanceZero ramp-up latency
GPU governorsimple_ondemandperformanceConsistent clock for GPU workloads
VNCTigerVNC on :2, SSH tunnel, localhost-only, auto-restartStable headless remote access
VNC clipboardautocutsel bidirectionalCopy/paste between Windows and VNC session
VNC connectPowerShell script with port polling + -4 SSH flagOne-click, no prompts, correct resolution
VNC auto-exitTCP connection watcher in PowerShellCloses vncviewer cleanly when session drops
PolkitSilent rule for NetworkManagerNo popup on every VNC login
Audio routingauto_null PulseAudio null sink + vnc-audio-switch.shAudio follows VNC connect/disconnect automatically
Audio streamparec → SSH pipe → pacat.exe → Windows PulseAudioLow-latency audio without opening extra network ports
PipeWire TCPDisabled in pipewire-pulse.confPrevents conflict with SSH audio pipe