The Debian root: how it works, and why¶
What the Debian root's units and settings do, and what goes wrong without
each. The quick start is firmware-modern/debian/;
to rebuild the root, see rebuild the Debian root.
Why the board moved off Buildroot is in debian-rootfs.md.
Transmitter safety at boot¶
The AD9361 can transmit at about +19 dBm, so the transmitter is held at maximum attenuation (−89.75 dB) from power-on until a program deliberately starts a transmit stream:
| covers | |
|---|---|
| the device tree | probe: adi,tx-attenuation-mdB = 89750, applied near the end of ad9361_setup(). The TX calibration just before it transmits for a few milliseconds at every power-on (IDLE-CASES.md) |
fishball-rf-quiesce.service |
from boot until something opens a transmit buffer |
| the kernel | patches/0004 mutes when a transmit buffer stops, 0015 when the DAC runs out of samples |
fishball-rf-quiesce waits up to 10 s for ad9361-phy, writes −89.75 dB to
both channels, reads them back, and sets the 60 s bound on unattended cyclic
transmits (tx_cyclic_timeout_ms, where the kernel has it). It is ordered before
iiod, the one process that opens transmit buffers unasked, and iiod has
Requires=fishball-rf-quiesce.service: if the quiesce fails, iiod does not
start. The board stays reachable over USB; journalctl -b -u fishball-rf-quiesce -u iiod
says why, and systemctl start iiod re-runs the quiesce first.
The USB route¶
The USB cable carries a network link (usb0, the board at 192.168.2.1), a
serial console and libiio, independently of Ethernet:
| Unit | Ordering | What it does |
|---|---|---|
fishball-usb-gadget.service |
Before=iiod.service |
builds the USB gadget, mounts FunctionFS |
fishball-usb-bind.service |
After=iiod.service |
attaches the gadget to the USB controller, brings usb0 up |
serial-getty@ttyGS0.service |
a login console on the same cable |
FunctionFS lets iiod serve libiio over USB from userspace. The gadget can
be attached only after iiod has written its USB descriptors (earlier fails with
EIO), so fishball-usb-bind waits for /dev/iio_ffs/ep1 and retries. With
iiod down it attaches the gadget without libiio, so usb0 and the console
still come up. iiod gets -F /dev/iio_ffs from a wrapper, fishball-iiod,
that checks the gadget exists, so a failed gadget cannot stop iiod.
The MAC addresses derive from sha1(hw_serial), as the factory S23udc
does, so your PC's interface name (for example enx00e022338e2c) and any
network profile bound to it stay the same. hw_serial lives in /mnt/jffs2 on
the QSPI flash, so it survives reflashing the card.
Board identity¶
fishball-identity.service mints hw_serial into /mnt/jffs2 once and writes
/etc/libiio.ini, whose values libiio serves as IIO context attributes (read by
tools/selftest/sdr_selftest.py and the MCP server).
fw_version carries the release (v2.1) and fw_build the full git describe
(v2.1-2-ge09d3add), both from /opt/VERSIONS. They are separate because
MATLAB's ADALM-Pluto support package fails to connect when fw_version looks
like a git describe string (matlab.md).
The login message¶
Every ssh or console login prints the VMAT logo, the boards this firmware is for
(PlutoSky R1, 7020-SDR, Fishball7020, Fish-Wan) and what the board is:
/etc/update-motd.d/10-fishball reads sysfs and files only, about 0.4 s.
| Line | Where it comes from |
|---|---|
| Firmware | fishball_build in /boot/uEnv.txt (the boot files) and device-fw in /opt/VERSIONS (the Debian root); a red line when the two differ |
| Release | the GitHub release those builds are, with a link to its page. A build is named by git describe, so v2.3 is release v2.3, and v2.0-9-g5ae29d94-dirty is not a release: 9 commits after v2.0, built with uncommitted changes, linked to v2.0's page. One line per part when the boot files and the root differ |
| Kernel | uname -r and its build date |
| FPGA | fishball_xsa, fishball_fpga and fishball_bitstream in /boot/uEnv.txt, named from a table of known designs |
| Ethernet, USB cable | the addresses of eth0 and usb0, as ssh and ip: URIs |
| Radio, Health | both transmit attenuators, the RX LO and rate, the AD9361 and FPGA temperatures, uptime, and a warning after any Calibration TIMEOUT (the sign of too little power) |
| Tune RX … Commands | the commands used most, and fishball-help for the rest: a cheat sheet grouped by job (look, receive, transmit, settings that survive a reboot, logs, services) |
| Tip | the next line of /usr/share/fishball/tips.txt at every login (a counter in /var/lib/fishball/motd-tip, so none repeats until all have shown); add your own there |
firmware-modern/build_all.sh stamps the four fishball_* lines into
uEnv.txt, so they travel wherever the boot files go: a release, write-card,
write-card.cmd, ./devkit flash. U-Boot imports them as variables nothing
reads. Boot files built before the stamp show as "unstamped"; the FPGA line then
comes from hashing the bitstream partition of /boot/BOOT.bin with
fishball-bootbin, the same parser the build uses, cached in /run.
/opt/VERSIONS records what was built: the device-fw line, the Debian release,
the build time and every installed package version.
/usr/share/fishball/packages.txt records what was asked for, and why. The base
image is pinned by digest and packages come from a fixed snapshot.debian.org date
(BASE and DEBIAN_SNAPSHOT in the Containerfile). The board keeps the normal
Debian sources, so apt update there gets current packages.
No identity in the image¶
rootfs.tar is a release asset, so anything in it is on every board flashed
from it.
- SSH host keys are deleted from the image, or every board would share one published private key.
/etc/machine-idis empty, which tells systemd this is a first boot./var/lib/dbus/machine-idis a symlink to it.
| generates host keys when | |
|---|---|
sshd-keygen.service (Debian's) |
ConditionFirstBoot is true |
fishball-sshd-keygen.service |
/etc/ssh/ssh_host_ed25519_key is missing |
On a freshly written card Debian's unit does not fire, so
fishball-sshd-keygen is what generates the keys. Keep it. Both run
ssh-keygen -A, which only creates missing keys.
Differences from Buildroot and from a container image¶
Each of these fails quietly or blames the wrong thing, so the fix is in the image.
| symptom | cause | what the image does |
|---|---|---|
no network, networking.service failed |
systemd renames eth0 to end0; Buildroot did not |
net.ifnames=0 on the kernel command line |
| avahi publishes the wrong hostname | podman bind-mounts /etc/hostname and /etc/hosts during a build, so writes to them never reach the image |
both files are in overlay/ |
| host streaming capped at 16 MB buffers | libubootenv's fw_printenv exits 0 with empty output for an unset variable, so an \|\| default never fires |
fishball-identity treats empty output as unset |
| ssh dies partway through a tool run, "Permission denied" | OpenSSH 9.8+ PerSourcePenalties blocks an address that opens many short connections, which the host tools do |
turned off in sshd_config.d/fishball-penalties.conf |
| PAM refuses a correct password | the board has no real-time clock and boots with a date before the image was built, so the password looks changed in the future | the root password is dated 1970-01-02 (chage -d 1 root) |
apt update rejects Release files as "not valid yet" |
the same wrong clock | systemd-timesyncd is installed in the image, so the clock is set before apt runs |
| installed services never start | the Debian container image ships /usr/sbin/policy-rc.d, which blocks service starts |
it is deleted |
Shutdown and reboot¶
Without these, a reboot can stall for minutes in stop jobs and then not reset:
etc/network/interfacesusesallow-hotplug eth0, notauto eth0, sonetworking.servicedoes not wait for a cable.etc/systemd/system.conf.d/fishball.confsetsDefaultTimeoutStopSec=20sandRebootWatchdogSec=60s, so the Zynq watchdog resets the board if shutdown stalls anyway.systemd-logindis masked. Nothing on the board uses it, and it could spin at start-up and restart forever, starving PID 1. A start-limit drop-in stays alongside for anyone who unmasks it.
A boot to login takes about 14 s and a reboot about 45 s.
Logs are in the systemd journal, not in /var/log/*.log. There is no syslog
daemon, so /var/log holds only the journal (/var/log/journal/, persistent,
capped at 32 MB, flushed to the card every 10 minutes) and a few package logs.
Read it with journalctl -b (this boot), -f (live), -u iiod (one service),
-k (the kernel), -b -1 (the previous boot) or -p warning (problems only).
The board has no real-time clock, so timestamps before the network clock syncs
can be wrong; do not subtract times across boots.
Pulling the power corrupts the journal. journald keeps corrupt archives as
*.journal~, journalctl stops reading at the first one, and they count against
the 32 MB limit:
# run from: the board
journalctl --disk-usage
rm -f /var/log/journal/*/system@*.journal~
A rare boot hang: RCU stops early in boot¶
| symptom | after a reboot the board never comes back (no USB device, console or network), but the USER LED keeps blinking steadily: the kernel heartbeat, so the kernel runs and userspace has stalled. Seen once in about 25 boots |
the journal of that boot (journalctl -b -1 -k after a power cycle) |
WARNING ... at kernel/rcu/tree.c:3094 call_rcu about 9 s in, then INFO: task (mount) blocked for more than 20 seconds (mounting /mnt/jffs2) and dev-ttyGS0.device timing out |
| cause | RCU (read-copy-update) is how the kernel frees shared data safely; once it stops, anything waiting on it waits forever. Why it stops, a kernel bug or a hardware glitch, is unknown |
| fix | power-cycle. The serial console on the DEBUG port would show the boot as it hangs |
If it happens again, save the evidence first:
# run from: the board, after the power cycle
journalctl -b -1 -k --no-pager > /root/hang-$(date +%s).txt
SD card writes¶
The Debian root writes to the card (Buildroot ran from RAM). Every mount is
noatime, the root is commit=30 (at most 30 s of writes lost to a power cut,
silently), and journald is capped at 32 MB and syncs every 10 minutes. Run
sync after deploying anything you care about. apt works but dpkg takes
minutes on the dual Cortex-A9.