This page describes the SatLink system as a whole, from the FPGA fabric up to the network services. It is the map to keep in mind when reading a telemetry value or a report.
The Zynq, split in two#
flowchart TB
subgraph PS["PS: dual Cortex-A9, Linux"]
sd[satlinkd]
uio["/dev/uio0 (1 MiB register window)"]
iio["IIO: satlink-modem-tx / -rx, ad9361-phy, cf-ad9361-dds-core"]
sd --> uio
sd --> iio
end
subgraph PL["PL: xc7z020 fabric"]
shell["shell: AXI-Lite decode, IRQ, loopback mux"]
tx[tx_top]
rx[rx_top]
ch[channel_impair]
mon[monitor_top]
seq[profile_sequencer]
dmac["axi_dmac tx / rx / adc"]
ad["axi_ad9361 (LVDS)"]
end
uio -- "M_AXI_GP0, AXI-Lite" --> shell
iio -- "S_AXI_HP, DMA" --> dmac
shell --> tx & rx & ch & mon & seq
dmac --> tx
rx --> dmac
tx --> ad
ad --> rx
ad <--> chip((AD9361))- Control plane. The PS reaches the PL's registers through one 1 MiB AXI-Lite window at
0x4000_0000, mapped into user space by the kernel's UIO driver as/dev/uio0(namedsatlink).satlinkdreads and writes registers by name through a catalogue that shares its addresses with the daemon's own code (see PL Register Map). - Data plane. Bytes travel between the PS and the modem through ADI
axi_dmaccores, exposed by a SatLink kernel module (satlink-modem.ko) as two IIO buffer devices,satlink-modem-txandsatlink-modem-rx. A third DMA channel captures raw ADC samples for IQ captures and the spectrum view. - RF front end. ADI's
axi_ad9361core connects the fabric to the AD9361's LVDS bus. The AD9361 itself is configured by the Linuxad9361-phydriver over SPI; its DAC core appears ascf-ad9361-dds-core.
| Region | Address | Content |
|---|---|---|
| SatLink shell | 0x4000_0000, 1 MiB | Seven 64 KiB windows: shell, RX DDC, RX demod, TX, monitor, common (sequencer), channel |
axi_ad9361 | 0x4010_0000 | ADI interface core |
axi_dmac TX | 0x4011_0000 | PS → modem DMA |
axi_dmac RX | 0x4012_0000 | modem → PS DMA |
axi_clkgen | 0x4013_0000 | Clock generator |
The DMA wire format: le:u8/32#
The modem's byte streams are 8 bits wide, but the DMA moves 32-bit words. Each 32-bit little-endian word carries one useful byte in its low lane; the other three bytes are zero.
| Quantity | Value |
|---|---|
| IIO buffer length | 4096 samples ([datapath] buffer_length) |
| DMA block | 4096 × 4 = 16 384 bytes |
| Useful bytes per block | 4096 |
satlinkd expands and extracts the lanes for you: what you push through the API, the CLI or the
link service is plain bytes, and what the link service publishes is lane-extracted. Two
consequences matter:
- A block is only submitted to the DMA when it is full. Low-rate traffic must be flushed (see Datapath and Transmission).
- Frame lengths used for slot framing and transfer frames must divide 4096, or alignment is lost at the first flush.
Files for satlinkctl loopback and satlinkctl compare are in the raw le:u8/32 encoding,
unless compare --dense is given.
Clocks#
| Clock | Frequency | Domain |
|---|---|---|
fclk0 | 100 MHz | Control plane: AXI-Lite decode, registers, profile sequencer |
clk_fpga_2 (FCLK2) | 50 MHz | Modem datapath (modem_clk) and the DMA streams |
AD9361 l_clk | Set by the AD9361's data rate | The axi_ad9361 core |
Register accesses to modem blocks cross from fclk0 to the modem clock through a request and
acknowledge synchroniser; samples cross between the modem clock and the AD9361 clock through two
asynchronous FIFOs.
The three signal paths#
The chain target decides where the transmitter's samples go:
| Target | Path | Emits RF | Uses the AD9361 |
|---|---|---|---|
pl (PL loopback) | TX chain → channel emulator → RX chain, GLOBAL_CTRL[0] = 1 | No | No |
device-loopback | TX chain → AD9361 → internal digital loopback, before the mixers → RX chain | No | Yes (LVDS and DAC source only) |
air | TX chain → AD9361 → mixers → TX1 connector … RX1 connector → RX chain | Yes | Yes |
- The PL loopback is the reference: it has no analog path and no frequency or phase error, and it is the only path with the channel emulator.
- The device loopback proves the LVDS interface in both directions. The LOs, gains and attenuation have no effect on it at all, and nothing reaches a connector; calling it "RF" is a classic source of confusion.
- The air target is the only one that reaches the connectors. Your cabling decides whether the signal comes back into RX1 (an external loopback) or reaches another radio.
A profile's radio.loopback flag writes the loopback mux on every apply. Chain conditioning
applies the profile first and sets the mode afterwards, so the mux ends up where the target wants
it. See Chain Conditioning.
Shadow registers and the profile sequencer#
Most modem configuration registers are double-buffered. A write lands in the shadow bank, the datapath keeps running on the active bank, and the profile sequencer swaps them in one cycle when asked. Its states:
SEQ_STATE | Name | Meaning |
|---|---|---|
0x00 | IDLE | Nothing pending |
0x01 | STAGED | Writes staged |
0x02 | VALIDATING | Waiting for the software's validation bit |
0x03 | WAIT_SAFE | Waiting for a safe point: 64 cycles with no sample on the TX or RX stream |
0x04 | COMMIT | Swapping banks |
0x05 | REARM | Re-arming |
A commit only happens at a safe point. If some stream never goes quiet, the sequencer stays in
WAIT_SAFE and the apply times out: SEQ_STATE = 0x03 is therefore also a sign that the datapath
is jammed. Reads always return the active bank, so a staged write reads back as the old
value until it is committed.
Inside satlinkd#
satlinkd is one Rust process built from a set of crates under ps/crates/:
| Service | Crate | Role |
|---|---|---|
| PL driver | satlink-pl-driver | UIO register access, the register catalogue, write tracing (to /dev/kmsg when enabled) |
| Profile manager | satlink-profile, satlink-profile-manager | Load, validate and translate profiles; stage and commit |
| Radio runtime | satlink-radio-runtime | AD9361 control over IIO, NCO offsets, chain conditioning, tones, receiver recovery |
| Datapath | satlink-datapath, satlink-packet | IIO buffers, block assembly, le:u8/32, slot and stream framing, CSP/CCSDS packet building |
| Scenario engine | satlink-scenario, satlink-scenario-engine | Load and run scenarios, sample telemetry, evaluate assertions, write reports |
| Campaign engine | satlink-campaign, satlink-campaign-engine | Run campaigns, keep the cursor on persistent storage |
| Telemetry | satlink-telemetry | Poll PL and AD9361 metrics at poll_interval_ms |
| Link service | satlink-link, satlink-ccsds | ZeroMQ bridge, CCSDS transfer frames |
| IQ and spectrum | satlink-iq, satlink-spectrum | IQ captures and FFT from the ADC capture DMA |
| API | satlink-api | REST, WebSocket, OpenAPI |
| Store | satlink-store | SQLite database of runs, reports and profile revisions |
| Observability | satlink-observability | Logging, Prometheus exporter on port 9090 |
The run store lives at /var/lib/satlink/satlink.db, on the RAM disk: reports do not survive a
reboot. Export the ones you need (see Analysis & Reporting).
Campaign cursors live on the persistent /mnt/jffs2 partition so a campaign can resume after a
reboot it asked for.
Where things are configured#
| What | Where | Changed by |
|---|---|---|
| Radio configuration | Profiles, /opt/satlink/dsl/profiles/*.yaml | Firmware build, or PUT /api/v1/profiles/{name} (in memory only) |
| Test timelines | Scenarios, /opt/satlink/dsl/scenarios/**/*.yaml | Firmware build, or POST /api/v1/scenarios (written to the RAM disk) |
| Test roadmaps | Campaigns, /opt/satlink/dsl/campaigns/*.yaml | Firmware build |
| Daemon behaviour | /etc/satlinkd/satlinkd.toml | Firmware build (stellar-fw/overlay/etc/satlinkd/satlinkd.toml) |
| The modem itself | The bitstream, system_top.bin on the SD card | A PL build and a release |