Troubleshooting¶
Symptom → cause. Most of these cost real time to diagnose the first time.
What a healthy boot looks like¶
Read this before chasing anything in the logs. A boot with zero failed units
and every service working still prints a dozen alarming lines at priority
err and warning. None of them is a fault.
The test¶
A line that is there on every boot is scenery. Compare this boot with the previous one:
sudo journalctl -b -p err -o cat | sort | uniq -c # this boot
sudo journalctl -b -1 -p err -o cat | sort | uniq -c # the previous one
A line that is new is the one worth reading. The count is a weaker signal than
it sounds: on the board this was last checked on (10 October 2026, kernel
7.0.0) one boot had 13 lines at err and the next had 17, and the whole
difference was the tmux-spawn line below, which is printed once for every
terminal pane you open.
The health check itself is short:
Lines every healthy boot prints¶
At priority err:
pm4125_codec 1c40000.spmi:pmic@0:audio-codec@f000: missing qcom,mbhc-buttons-vthreshold-microvolt entry
va_macro a730000.codec: qcom,dmic-sample-rate dt entry missing
qcom-soundwire a610000.soundwire: din-ports (0) mismatch with controller (1)
qcom-soundwire a610000.soundwire: dout-ports (5) mismatch with controller (6)
qcom-soundwire a740000.soundwire: din-ports (3) mismatch with controller (4)
MultiMedia1: ASoC: no backend DAIs enabled for MultiMedia1, possibly missing ALSA mixer-based routing or UCM profile
qcom_q6v5_pas ab00000.remoteproc: timeout waiting for ssctl service
/usr/lib/udev/rules.d/90-alsa-restore.rules:18 GOTO="alsa_restore_std" has no matching label, ignoring.
CPUidle PSCI: failed to create CPU PM domains ret=-517
CPUidle PSCI: CPU 0 failed to PSCI idle
CPUidle PSCI: Failed to create psci-cpuidle device
Bluetooth: hci0: Frame reassembly failed (-84)
Failed to start tmux-spawn-….scope - tmux child pane … launched by process …
And at warning, the two that look worst:
! Random seed file '/boot/efi/loader/random-seed' is world accessible, which is a security hole! !
File /var/log/journal/…/system.journal corrupted or uncleanly shut down, renaming and replacing.
| Lines | What it is |
|---|---|
pm4125_, va_, qcom-soundwire, MultiMedia1: ASoC |
The vendor devicetree describing audio hardware this board does not expose. Not this project's. |
90-alsa-restore.rules … no matching label |
A Debian packaging wart in the ALSA udev rules. |
CPUidle PSCI: … |
Real, and not harmless in the long run: the CPU cores have no idle states on this kernel. It breaks nothing you will notice. Tracked as issue #39. |
Random seed file … world accessible |
Real, and small. /boot/efi is FAT, which has no permission bits, so every file on it reads as 755 whatever you chmod. Tracked as issue #40. |
system.journal corrupted or uncleanly shut down |
The board lost power without being shut down, which is how most people turn it off. It reads like data loss and is the journal starting a new file. |
tmux-spawn-….scope |
From your login session, not the board: a cgroup delegation problem in systemd --user. One line per terminal pane. Harmless to the services. |
qcom_, Bluetooth: hci0: Frame reassembly failed |
Seen on consecutive healthy boots. Nobody has explained them. |
The last row is listed because those lines pass the test above, not because anyone knows what they mean.
Two minutes of userspace is deliberate¶
systemd-analyze reports a little over two minutes of userspace time. That is
not a slow boot. unoq-usb-confirm.service waits 120 seconds on purpose before
it declares the boot healthy and clears the bind guard's counter, and the boot
is not "finished" until it has:
Everything else is up long before that. See From power to prompt.
What each unit logs when it is working¶
Taken from a healthy boot with the USB-C port in host mode and the board on wifi. With the port plugged into a computer the USB lines differ: there will be a bind attempt, and a lease.
unoq-link UNO Q link up: BOOT0(line 37)=0 UART-enable(line 70)=1 on /dev/gpiochip1
unoq-usb-gadget gadget built (c.1 = ncm.usb0 + mass_storage)
built only, not binding (--build)
created br-usb
no gadget interfaces yet - the bridge is up and waiting
asking for 192.168.137.210 again (last address on this link)
unoq-usb-confirm already confirmed
unoq-uplink-fallback wifi is already on - nothing to do
unoq-learn serving /srv/unoq-share on port 8080
Three things here read like problems and are not.
built only, not binding and no gadget interfaces yet look like a failure
to start. They are the gadget being prepared and then waiting. Nothing can
bind until the port is plugged into a computer; see usb.md.
unoq-uplink-fallback logging at all looks like a fallback fired. The
opposite: it checked, and there was nothing for it to do.
bind attempt 1/3, when the port is plugged into a computer, looks like a
retry. It is the first and only attempt. The counter belongs to the bind
guard, and unoq-usb-confirm clearing it two minutes later is the success
signal.
unoq-usb-bind, unoq-usb-confirm and unoq-uplink-fallback show as
inactive (dead) once they have run. They are one-shot jobs, and that is what
finished looks like.
The board looks dead¶
Firmware flashed and "Verified OK", but nothing runs.
BOOT0 (gpiochip1 line 37) is high or floating, so the STM32 booted its ROM
bootloader instead of your image. Confirm by reading the PC — anything around
0x0bf9xxxx is ROM, not flash:
/opt/openocd/bin/openocd -s /opt/openocd -f /opt/openocd/openocd_gpiod.cfg \
-c init -c "reset run" -c "sleep 1000" -c halt -c "reg pc" -c shutdown
Fix: ~/two-computers-one-board/mcu/link-up.sh, then re-flash. Check unoq-link.service is
enabled so it survives reboots.
/dev/ttyHS1 reads zero bytes, but the MCU seems fine.
The UART link-enable (gpiochip1 line 70) is not driven high. The MCU
transmits happily into a disconnected path. Same fix: link-up.sh.
To prove the MCU is actually alive and transmitting, check LPUART1 — CR1
should show UE/TE/RE set (0x2d) and ISR should show TC/TXE:
The link was fine until I inspected it.
You ran gpioget on line 37 or 70. It requests the line as an input, which drops
the drive — reading these pins breaks them. The values it returns (37=active,
70=inactive) look like a diagnosis but are just the floating state you created.
Fix: link-up.sh. Check them non-destructively instead:
See hardware.md.
Device or resource busy on /dev/ttyHS1, or PortBusy from Python.
Something else holds it — the unoq-cpu-bars service, tio, mcucon, a stale
Python handle, or (if it were re-enabled) arduino-router. The usual answer is
sudo systemctl stop unoq-cpu-bars; the full entry, with the message text, is
under LED matrix. If nothing appears to hold it,
see the note on tio.
LED 3 is yellow. The MCU is running a firmware image that has not been confirmed, so the next reset puts the previous one back. That is what an update over serial looks like until you confirm it. To keep the image:
sudo systemctl stop unoq-cpu-bars
hpy -c "from unoq import fota; fota.confirm(); print(fota.images())"
sudo systemctl start unoq-cpu-bars
To reject it, reset the MCU without confirming. See mcu.md.
LED 3 and LED 4 are dark, and so is the matrix.
The microcontroller is not running this project's firmware: nothing is driving
its two LEDs. Within about two and a half minutes LED 2 starts blinking red
as well, because the demo gives up trying to reach it and
systemctl --failed lists unoq-cpu-bars.service. Put the firmware back,
then start the demo again:
~/two-computers-one-board/mcu/zbuild.sh && ~/two-computers-one-board/mcu/flash-all.sh
sudo systemctl reset-failed unoq-cpu-bars && sudo systemctl start unoq-cpu-bars
The one known cause that is not something you did: Arduino's
arduino-burn-bootloader.service, which writes the stock bootloader over the
chip at boot. Setup disables it; systemctl is-enabled arduino-burn-bootloader
should say disabled.
LED 3 is red.
A part of the firmware failed to start. At present that means the LED matrix:
the MCU's boot output says LED matrix unavailable. See
LED matrix.
LED 4 is red.
The MCU has not received one of the firmware's app commands for four seconds.
On a board without unoq-cpu-bars that is the normal state: nothing is talking
to it. On a board with the service, check it is running:
If the service is running and LED 4 is still red, the serial link is not
carrying data. Run ~/two-computers-one-board/mcu/link-up.sh and see the
entries above.
LED 2 is red. Something needs a person: a systemd unit has failed, or the USB bind guard has reached its limit.
Fix the unit and sudo systemctl reset-failed <unit>, or see
the bind guard. The LED follows within about five
seconds. What all four LEDs mean: ~/two-computers-one-board/status/leds.sh
explain.
Serial output is wrong or missing¶
A stock Zephyr sample prints nothing.
Upstream defaults zephyr,console to usart1, which is the Arduino header
pins, not the MPU link. Override to lpuart1 — see
mcu/app/boards/arduino_uno_q.overlay.
Shell prompt fights with application output.
Do not printk on a timer when the shell is enabled. Expose a shell command
instead and let the MPU poll it.
Build and flash¶
nothing built yet: … zephyr.signed.hex is missing - run zbuild first.
zflash (mcu/flash.sh) with no image named flashes this project's own build,
and there is not one yet. Run zbuild, then zflash again.
openocd missing at /opt/openocd/bin/openocd. flash.sh looks for OpenOCD
under /opt/openocd, and it is not there. Check with ls /opt/openocd/bin/. To
build it again, see Rebuilding OpenOCD. If
you have a copy somewhere else, name it: OCD_ROOT=/path/to/openocd zflash.
MISSING: <tool> - run tools/install-dev-tools.sh from tools/check.sh. A
quality gate's tool is not installed, and a missing tool is a failure, not a
skip. Run the command it names:
~/two-computers-one-board/tools/install-dev-tools.sh (no sudo, no apt).
west flash fails with stm32cubeprogrammer not found.
That is the board's default runner and it is not installed. Use -r openocd,
or zflash.
west flash -r openocd crashes in samefile().
Zephyr's runner resolves board support in-tree and uno_q ships no support/
directory (runners/openocd.py:92). Restore it:
cp -r ~/two-computers-one-board/mcu/board-support/support ~/zephyrproject/zephyr/boards/arduino/uno_q/
Needed again after a Zephyr version bump.
Two errors on every flash:
Translation from khz to adapter speed not implemented and
Execution of event reset-init failed. Harmless — the bitbang adapter has no
configurable speed. Programming and verify still succeed.
Board stops booting after flashing.
You probably flashed the unsigned zephyr.hex over the MCUboot chain. It links
into slot0 with no image header, so the bootloader refuses it. Recover with
~/two-computers-one-board/mcu/flash-all.sh.
native_sim fails with a CONFIG_64BIT error.
Use native_sim/native/64 — the plain target is 32-bit and this is aarch64.
Error finding board: arduino_uno_q / No module named 'jsonschema'.
Something configured mcu/app with a bare cmake instead of west, so
list_boards.py ran under /usr/bin/python. Zephyr's script dependencies live
only in ~/zephyrproject/.venv, which is the interpreter west uses.
Usually the culprit is the CMake Tools extension: it prompts on open, writes
cmake.sourceDirectory into .vscode/settings.json itself, and then configures
into a stray two-computers-one-board/build/ on every window open. It is listed under
unwantedRecommendations — uninstall it, and delete both the setting and the
directory. Build with the MCU: build task (zbuild.sh); clangd needs
nothing from CMake Tools, only the compile_commands.json that build symlinks.
SMP / MCUmgr¶
mcumgr: command not found in the shell.
Expected. SMP-over-shell registers no command; the shell sniffs SMP frames out
of the byte stream.
SMP times out right after using the shell.
The UART needs a settle delay between handles. unoq does this for you; raw
scripts must close, sleep, then open.
MCUmgr features silently missing.
CONFIG_MCUMGR_TRANSPORT_SHELL disables itself without CONFIG_BASE64 and
CONFIG_CRC. Kconfig warns but the build succeeds — check the generated
.config, not just the build log.
LED matrix¶
The panel stays dark.
It starts lazily — nothing is lit until the first app bars or app matrix px.
After that, check sweeps in app status is climbing by ~962/s: a count stuck
at zero means the refresh timer never started, and the boot log says LED matrix
unavailable if gpiof or timers17 were missing from the devicetree.
The bars hang from the top instead of standing on the bottom.
The panel's orientation depends on how the board is mounted and the firmware
cannot know it. app matrix flip rotates 180° and stores that in NVS.
app bars says range or usage.
One to seven values, each 0..100. unoq.MCU.bars() clamps percentages for you
but raises on the wrong number of bars — see mpu.md.
Something else on PF0..PF10 misbehaves. Those eleven pins are the matrix. The refresh ISR retakes them every 10 µs, so any other user of them will lose. PF11–PF15 are untouched.
The serial port is busy¶
/dev/ttyHS1 is "device busy" — mcucon, tio and unoq.MCU all fail.
The boot service is probably running and holding the port open.
sudo systemctl stop unoq-cpu-bars, and start it again when you have finished.
Flashing over SWD is unaffected.
Since #55 unoq.MCU and mcucon report this properly. They raise PortBusy,
which names the process holding the port and the command to stop it, instead of
surfacing a bare errno 16:
unoq.mcu.PortBusy: /dev/ttyHS1 is flagged exclusive. Open right now: unoq-cpu-bars (pid 812).
Stop it first - for the demo that is: systemctl stop unoq-cpu-bars
The other form of the message begins the same way and continues but no process
this user can see has it open. That is the case in the note below, and the
message ends with the fix: systemctl restart unoq-cpu-bars.
tiocan leave the port unusable after it exits¶This is the confusing one, and it looks like a hardware fault.
There are two different locks on a serial port and they do not see each other.
flock— which is what pyserial'sexclusive=Truetakes — is advisory: it stops another pyserial caller and nothing else.TIOCEXCL— which is whattioandscreenset — is enforced by the kernel, and any lateropen()fails withEBUSY.
TIOCEXCLis per-tty state, cleared only when the last file descriptor closes.unoq-cpu-bars.serviceholds the port open permanently. So atiothat is killed rather than quit cleanly can leave the flag set with nothing to show for it:$ sudo lsof /dev/ttyHS1 unoq-cpu- 255481 arduino 3uW CHR 237,1 0t0 149 /dev/ttyHS1 $ hpy -c "import os; os.open('/dev/ttyHS1', os.O_RDWR)" OSError: [Errno 16] Device or resource busyThe only process listed is the one that has always had it open and is not the cause. Nothing in
lsofexplains theEBUSY, because the flag outlives the process that set it.
unoq.MCUnow takes both locks itself (open_port()inpython/unoq/mcu.py), which mostly closes this off:tiocan no longer open the port while the service is running, so it cannot flag it.tiowaits silently rather than reporting the refusal — it retries by default, so it looks like a hang. That is the port being held, not a fault.
The panel is stuck showing an old frame.
Something killed the daemon with SIGTERM instead of SIGINT, so the cleanup that
blanks it never ran. app matrix off, or hpy -c "from unoq import MCU;
MCU().matrix_off()". The systemd unit sets KillSignal=SIGINT to avoid this.
You edited a script and the board still does what it did before.
The services that run as root do not run the scripts in the checkout. They run
root-owned copies in /usr/local/lib/unoq/, and those change only when a
provisioning step is run. usb/status.sh says whether the two differ, under
what root runs. To refresh every copy:
When each script then takes effect is listed in Trying a script change on the board.
A provisioning step warns that units "still run as root from the checkout".
The board was set up before root had its own copies, and the step that
installed those units has not been run since. Run the step the warning names.
A run with --no-usb-gadget or --minimal does not run 95-usb-gadget.sh,
so the four USB units need that step, or a plain ./bootstrap.sh.
A service fails with status=203/EXEC after you move the checkout.
A virtualenv is not relocatable. Its console scripts carry an absolute
shebang, so after mv ~/old-name ~/new-name the file at the new path still
begins #!/home/you/old-name/.venv/bin/python. systemd reports
which names the script, not the interpreter that is actually missing. Delete the venv and rebuild it — do not try to repair it in place:
rm -rf ~/two-computers-one-board/.venv
bash ~/two-computers-one-board/provision/user/35-python-venv.sh
bash ~/two-computers-one-board/tools/install-dev-tools.sh # dev extras
sudo systemctl restart unoq-cpu-bars unoq-learn
The rm -rf is not belt-and-braces. Re-running the install over a moved venv
fixes only the packages it reinstalls: uv sees everything else as already
present and never rewrites those shebangs. That leaves a venv where python
works and mypy, clang-format and mkdocs all fail with
which is execve reporting the interpreter is missing, not the script.
Re-running the provisioning scripts rewrites the units for the new path;
install_unit now refuses to install a unit whose interpreter is missing, so
this is caught at provisioning time rather than at the next boot.
unoq-cpu-bars: command not found.
The program is installed into the project's Python environment, at
~/two-computers-one-board/.venv/bin/unoq-cpu-bars, and that directory is not
on PATH. Type the full path, or use hpy -m unoq.cpubars. Stop the service
first if it is running: it holds the serial port.
USB and network¶
The USB link has its own list, with the one command that prints the whole picture: usb.md. The entries here are the messages you meet before you get that far.
<hostname>.local does not resolve.
First check the name: run hostname on the board, and use that with .local
on the end. The name is published by avahi-daemon on the board:
provision/95-usb-gadget.sh installs and enables it. If it is active and the
name still does not resolve from your computer, connect by number instead:
~/two-computers-one-board/usb/status.sh prints the board's address on the USB
link, and ip -br addr prints all of them.
no network access - the SDK, Zephyr and PyPI all need to be reachable
from bootstrap.sh. Its first check fetches https://github.com and gives up
after 20 seconds. Nothing has been changed yet. Get the board online, confirm
with curl -I https://github.com, and run ./bootstrap.sh again.
the gadget is not bound to a UDC - nothing would be left from
usb/wifi.sh off or check. The board is not a USB device to anything right
now, so turning wifi off would leave no way in. The script has refused, and
wifi is still on. Plug the USB-C port into a computer, then look at
~/two-computers-one-board/usb/status.sh.
no computer on br-usb: no gateway, and no IPv4 or IPv6 neighbor either
from the same command. The gadget is bound, and nothing on the other end of the
cable has answered. Check the cable carries data, and that the computer shows a
new network adapter. If the computer is meant to share its internet connection,
turn that on there.
REFUSING to bind: 3 consecutive boots never confirmed in the journal.
The bind guard has stopped the USB gadget binding, because the board kept
rebooting soon after doing so. Wifi has been turned back on so that you can
reach it. Cause, how to look, and the two commands that clear it:
the bind guard.
/srv/unoq-share has no content from provision/75-learning-web.sh or
./bootstrap.sh. The web server serves the drive image, and
the image has not been built. Build it, then run the step again:
sudo bash ~/two-computers-one-board/share/fetch-vscode.sh # ~1.8 GB, needs internet
sudo bash ~/two-computers-one-board/share/build-image.sh
sudo bash ~/two-computers-one-board/provision/75-learning-web.sh
Toolchain¶
apt install openocd does not work.
Debian ships 0.12.0 (2023), predating libgpiod v2. Rebuild from upstream master:
sudo bash ~/two-computers-one-board/tools/build-openocd.sh.
Packages look outdated but must not be upgraded.
cbor2 (<6 by smp), pydantic-core (pinned exactly by pydantic), and
Zephyr's own venv pins. See
Working on this project.
An extension "update" will not install.
--install-extension <id> --force does not bump versions — name the version
explicitly. If that reports "not found", that version has no build for this
board (linux-arm64); publishers routinely ship x64 and Windows first.
Do not read "not found" as "already current" — a newer arm64 build may
exist below the version you tried. check-versions.sh reports the newest
arm64-installable version, which is the one to name. To see every build:
Use …/server/bin/code-server to install; the remote-cli/code binary needs a
live VS Code session and is not on PATH.
Recovery¶
Back to stock Arduino firmware:
Works even though ~/.arduino15 is gone, provided you copied the stock image
off the board first. It is Arduino's build, so this repository does not ship it
— see third-party.md. Point STOCK_FW at your copy.
no stock MCU firmware found from provision/55-purge-arduino.sh or
./bootstrap.sh --with-purge. The purge deletes the only copy of Arduino's
firmware image on the board, so it refuses to run until one has been saved.
Nothing has been removed. The message prints what to do; in short:
find ~/.arduino15 -name '*stm32u585xx*.hex'
mkdir -p ~/uno-q-backup && cp <that file> ~/uno-q-backup/
then run the purge again. If the image is already gone and you accept that,
sudo UNOQ_ALLOW_NO_STOCK_FW=1 bash ~/two-computers-one-board/provision/55-purge-arduino.sh
(the variable goes after sudo, or it does not reach the script).
bootstrap.sh prints a shorter warning with the same first words during its
preflight; that one is only a warning, and it carries on.
No network reaches the board at all.
If the USB gadget will not bind and wifi is off, the MPU's serial console,
/dev/ttyMSM0, still offers a login and shows kernel messages from the first
boot line. Which pads it comes out on is not yet verified — see
hardware.md.