SatLink runs on a Zynq-7020 (xc7z020clg400-1) board carrying an AD9361 transceiver. This page
covers the boards, how the board boots, how to reach it, and how to cable its RF ports.
Two boards, two pinouts#
| Board | Role | Constraint file |
|---|---|---|
| LibreSDR / zynqsdr rev5 | The SatLink product PCB | pl/top/constraints/zynqsdr_rev5.xdc |
"fishball" (a Pluto-class clone, fishball7020) | The development board all on-board validation has been done on | pl/top/constraints/zynqsdr_fishball.xdc |
Both use the same Zynq package, but the AD9361 is wired to different pins. A bitstream built
with one board's constraints does not work on the other: on the fishball, a rev5 bitstream leaves
the AD9361 without SPI, the driver reports Unsupported PRODUCT_ID 0x0, and the whole RF and
clock chain stalls. Always build with the constraint file of the board you deploy to (see
Building the Bitstream).
Booting from the SD card#
The board boots from the SD card, never from QSPI flash, so a bad image can always be recovered
by rewriting the card. The firmware is built by the separate stellar-fw repository on top of the
official Analog Devices plutosdr-fw v0.39 (Linux 6.1-adi, u-boot, Buildroot), into which it
injects the SatLink bitstream and satlinkd.
The SD card carries a single FAT partition labelled SATLINK. u-boot loads the bitstream
(system_top.bin), then the kernel, the device tree (devicetree.dtb) and the root filesystem
(uramdisk.image.gz).
The root filesystem is a RAM disk. Anything written to /tmp, /usr/bin, /var/lib/satlink or
/opt/satlink is lost at the next reboot. The one persistent writable place is /mnt/jffs2, a
small (about 900 KB) data partition on the QSPI flash, outside the boot chain. The daemon keeps
campaign cursors there; the watchdog keeps its reset log there.
What ships on the board:
| Path | Content |
|---|---|
/usr/bin/satlinkd | The daemon, started at boot by /etc/init.d/S99satlinkd |
/etc/satlinkd/satlinkd.toml | Its configuration (see Daemon Configuration) |
/opt/satlink/dsl/profiles/ | Radio profiles (copied from dsl/profiles/ at firmware build time) |
/opt/satlink/dsl/scenarios/ | Scenarios (from dsl/scenarios/, loaded recursively) |
/opt/satlink/dsl/campaigns/ | Campaigns (from dsl/campaigns/) |
/var/log/satlinkd.log | The daemon's log (RAM disk) |
/mnt/jffs2/ | Persistent: campaign state, reset-log, heartbeat, stellar.conf overrides |
Reaching the board#
The board answers on two network interfaces at once:
| Interface | Address | Notes |
|---|---|---|
| USB gadget (RNDIS/ECM over the OTG port) | 192.168.2.1 | Point to point; the host side is on 192.168.2.0/24 |
eth0 (the Zynq's own Ethernet) | DHCP, hostname satlink | Needs a cable; the lease arrives after Starting network: OK, so a lookup issued too early finds nothing |
Services on the board:
| Port | Service |
|---|---|
8080 | REST API (/api/v1/...) and WebSockets (/ws/...) |
5555 | ZeroMQ PULL: frames in, relayed to the modem |
5556 | ZeroMQ PUB: frames received by the modem |
5557 | ZeroMQ PUB: telemetry snapshots |
9090 | Prometheus metrics exporter |
22 | SSH (Dropbear), user root, the stock Pluto password analog |
Point the CLI at the board with SATLINK_API:
export SATLINK_API=http://192.168.2.1:8080 # or http://<eth0 address>:8080
satlinkctl statusIf the API stops answering, check which interface you are using before suspecting the daemon:
the USB gadget can drop off the host's bus while the board keeps serving on eth0. One curl
per address separates the two.
Resolve the board by identity, not by address#
An address is a hint. Other Pluto-class boards answer on 192.168.2.1 as well, and a bench can
have more than one board on the same network. Reconfiguring the wrong one drives someone else's
radio and reports the result as your measurement.
The board's IIO context is named SatLink SDR. List the IIO contexts on the network and pick
the one with that name, then check its serial number:
iio_info -S # lists contexts: name, serial, address
pl/top/bringup/resolve-board.sh # prints the board's IP, or refusesresolve-board.sh takes the unique context named SatLink SDR, cross-checks the serial pinned in
the script, retries while mDNS discovery is still settling, and refuses when two different
serials answer to the same name. The same board appearing several times (USB gadget, IPv6
link-local, raw USB) is not ambiguous: it is grouped by serial. Edit the pinned serial when you
set up a new board.
The board's own safeguards#
Hardware watchdog#
The firmware arms the Zynq's hardware watchdog at boot (10 s timeout, petted every 5 s). A board that reboots on its own is therefore not necessarily a crash: it may be the watchdog recovering a freeze. Before theorising about a reboot, read the reset log:
ssh root@192.168.2.1 cat /mnt/jffs2/reset-logIt records the SoC's reset reason at each boot (bit 16 = watchdog, bit 22 = power-on,
bit 19 = software reboot). /mnt/jffs2/heartbeat holds what the previous boot knew about a
minute before it stopped (uptime, load, whether satlinkd was alive, DMA interrupt counts).
To keep a hang visible on the serial console instead of being reset silently, write
WATCHDOG=off into /mnt/jffs2/stellar.conf. The setting survives reboots.
The clock#
The board has no real-time clock. At power-on its clock reads 1 January 1970, and every
timestamp the board emits counts from boot. The firmware can persist the time across reboots
(S26clock) and query NTP when NTP_SERVERS is set, but it must be seeded once, for example
with date -s or ntpd -q against a reachable server. Until then, treat every board timestamp
as uptime. See Telemetry and Metrics.
Cabling the RF path#
The modem uses TX1 and RX1 of the AD9361, on input port A (A_BALANCED). The device
tree locks the RX port selection; ports B and C carried no signal on the reference bench. The
second channel (TX2/RX2) is not used by the modem.
The reference bench cabling is:
board TX1 --> 40 dB pad --> 2-way splitter --+--> spectrum analyser
+--> board RX1With both splitter arms terminated, the measured loss from TX1 to either arm was about 44 dB at
437 MHz (40 dB of pad plus the splitter). The board's own output was calibrated as
P_board ≈ att + 15.8 dBm, where att is the AD9361 TX attenuation (0 to −89.75 dB). With TX
attenuation between −30 and −10 dB, RX1 then sees roughly −58 to −38 dBm, far from the input's
limit.
Useful rules when cabling:
- Characterise, do not assume. A pad's marking and a splitter's datasheet do not add up to the real loss, and the loss is frequency dependent. Re-measure whenever a connector changes (see Measurement Tools).
- A spectrum analyser's reference offset is additive. A pad of N dB is compensated with +N dB, never −N.
- Match the receive gain to the level at RX1. The profile's
radio.rx_gain_dbmust bring the signal into the PL's AGC range. If the telemetry showsagc_gainpinned near 16.0 while traffic flows, the receiver is starved (see Telemetry and Metrics).
The RF kill switch#
POST /api/v1/radio/rf/kill (also a button in the web console header) takes RF off the
connector: it puts the AD9361's state machine in alert, closes the TX egress, stops the TX chain
and disarms RX, reading each step back. TX attenuation alone does not remove RF: at −89.75 dB the
part still emits, and an idle DAC still leaks the LO.
To come back, run satlinkctl chain up (see Chain Conditioning).
Note that a profile apply re-opens the TX egress: if you engaged the kill switch deliberately for
cabling work, engage it again after any apply.
Optional bench equipment#
| Equipment | Used for |
|---|---|
| A switchable USB hub (for example a YKUSH) | Cutting the board's power from a script, so a campaign can recover from a freeze. Some freezes need all ports cut, not just the board's |
| A JTAG probe | Loading debug bitstreams and reading ILA captures (see Building the Bitstream) |
| A spectrum analyser | Characterising the RF path and checking the transmitted spectrum |
| A second SDR (for example a bladeRF) | An independent reference transmitter or receiver, to tell a transmitter defect from a receiver defect |