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:
| Test | Result |
|---|---|
| CPU + VM 20% load, no freq cap | Crash in ~30 seconds |
| CPU-only 20% load | 10 minutes stable |
| CPU-only 80% load, no freq cap | Crash |
| CPU + VM 100% load, cap 1.608 GHz | Stable |
| CPU + VM 100% load, cap 1.8 GHz | Stable |
| CPU + VM 100% load, cap 2.0 GHz | Crash ~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.servicedisabled.
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:
-4forces 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
-RemoteResizematches the VNC session to whichever monitor it opens on- Audio streams raw PCM over a second SSH connection (see Chapter 4)
pulseaudio.exeis 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
HasExitedfires
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:
pactl set-default-sinkchanges where new streams go.move_streams_toexplicitly 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:
- SSH tunnel opens (VNC on port 5902).
- Audio SSH pipe starts —
parecon SBC streams topacat.exeon Windows. vnc-audio-switch.shdetects the new connection and switches the default sink toauto_null, moving any already-playing streams.- VNC session opens with audio already playing through Windows speakers.
When the session closes:
- VNC TCP session drops;
vnc-connect.ps1auto-exits. - Audio SSH pipe is killed (
taskkill /Ton the cmd.exe wrapper). vnc-audio-switch.shdetects port 5902 gone and switches back to the physical HDMI sink.- Audio resumes on the TV.
Summary #
| Layer | Change | Effect |
|---|---|---|
| Power supply | Replaced underpowered supply with 5 V / 4 A unit | Board stable; 3.7× stress throughput vs. capped workaround |
| CPU governor | schedutil → performance | Zero ramp-up latency |
| GPU governor | simple_ondemand → performance | Consistent clock for GPU workloads |
| VNC | TigerVNC on :2, SSH tunnel, localhost-only, auto-restart | Stable headless remote access |
| VNC clipboard | autocutsel bidirectional | Copy/paste between Windows and VNC session |
| VNC connect | PowerShell script with port polling + -4 SSH flag | One-click, no prompts, correct resolution |
| VNC auto-exit | TCP connection watcher in PowerShell | Closes vncviewer cleanly when session drops |
| Polkit | Silent rule for NetworkManager | No popup on every VNC login |
| Audio routing | auto_null PulseAudio null sink + vnc-audio-switch.sh | Audio follows VNC connect/disconnect automatically |
| Audio stream | parec → SSH pipe → pacat.exe → Windows PulseAudio | Low-latency audio without opening extra network ports |
| PipeWire TCP | Disabled in pipewire-pulse.conf | Prevents conflict with SSH audio pipe |