The SpaceWire mission data link
The spacecraft has two buses, and the reason is bandwidth against determinism. The CAN backplane is a command-and-control bus: eight octets a frame, arbitrated by identifier, and paced so a science readout can never starve the control path — about 5.6 kB/s in practice. It’s the right bus for a mode change and the wrong one for an image. SpaceWire (ECSS-E-ST-50-12C) is the high-rate point-to-point link between the payload card and the DHS’s mass memory, carrying RMAP (ECSS-E-ST-50-52C) writes that put science octets straight into the mass memory unit. Nothing that isn’t bulk mission data travels on it, and the bulk data never touches CAN.
The control plane is PUS service 23, file management: the ground lists a repository, inspects a file, and commands a copy. The bytes then move with no further ground involvement.
Source: include/spacewire.h (the link: datagram, character level, flow
control, state machine, logical addressing, protocol identification),
include/rmap.h (RMAP encode/decode and CRC-8),
common/spw_drv.c (the node-side driver, where the
Posix port workarounds live again), nodes/payload_node.c
(the RMAP initiator) and nodes/dhs_node.c (the RMAP target, the mass memory
file partition and service 23).
1. Topology
Point-to-point, with no router process — unlike the CAN bus, which has can_hub. That’s not a
simplification: SpaceWire is point-to-point at the link level, a routing switch is a separate
device, and this spacecraft has exactly two ends to connect. So the link is a UDP socket pair on
the loopback:
payload_node ──── UDP 127.0.0.1:20100 ────> dhs_node
SPW_LA_PAYLOAD 0x21 SPW_LA_DHS 0x20
RMAP initiator RMAP target
dials out from an ephemeral port binds the link portThe DHS binds because it’s the target: a target that had to go looking for its initiator would have nothing to bind to before the first transfer. This is the same asymmetry TTC uses toward the EGSE. Both addresses being compile-time constants on the loopback, the Docker arrangement needs no change — the containers already share one network namespace and nothing here is published.
The driver is linked into two nodes only, through the ${ARGN} argument of add_freertos_node
rather than the shared common/ list. The ADCS, EPS and TTC have no SpaceWire port, and compiling
one into them would be describing hardware they don’t have.
2. Character level
A real SpaceWire link carries data characters (N-chars) and control characters (L-chars). Modelling that octet by octet over UDP would simulate the encoding rather than the link, so each datagram carries either one whole packet’s worth of N-chars or a run of one kind of L-char, and says which:
[0] kind SPW_DG_PACKET | SPW_DG_CTRL
[1] arg end marker (SPW_EOP | SPW_EEP), or the L-char (NULL | FCT | DISC)
[2..3] body length, big-endian
[4..] bodyEOP and EEP. A packet ends with a normal end-of-packet marker or an error one. An EEP-terminated packet is still delivered to the layer above, deliberately — discarding it silently would leave the far end waiting for a reply to a command nobody admits to having seen. The RMAP target turns an EEP into status 7 and answers.
3. Flow control
One FCT grants credit for exactly eight N-chars — the standard’s number, not a tuning knob. The
receiver emits FCTs for the room it has in its 8 KiB receive buffer; the transmitter decrements its
credit per octet sent and may not transmit an octet it has no credit for. There is no other
“ready to receive” handshake, and a transmitter that ignores this one overruns the far end. Credit
stalls are counted, and a transmitter that waits longer than SPW_TIMEOUT_MS fails the transfer
rather than hanging.
- FCTs are coalesced. One
SPW_DG_CTRLdatagram carries a run of FCTs, with the count in the length field — the character stream’s own behaviour rather than a shortcut around it, and 8 KiB advertised one datagram at a time would be 1024 datagrams. - Credit is waited for outside the transmit mutex. Blocking on an FCT while holding the lock would stop the receive task sending the very NULL that keeps the link up.
4. Link initialisation
The standard’s six-state machine (50-12C 8.5.3.2), kept by name:
ErrorReset -> ErrorWait -> Ready -> Started -> Connecting -> RunOnly in Run may packets flow. The standard’s 6.4 µs and 12.8 µs timings are modelled as one and two ticks: the sequence of states is real, the durations are not.
Started -> Connecting -> Run takes two received NULLs — the handshake, not a delay for its own
sake: each end must see the other’s characters before it may send data, and the listening end has
nowhere to send a NULL until it has heard from the initiator, which is why the dial-out end always
speaks first.
Silence for SPW_TIMEOUT_MS (1 s) drops the link to ErrorReset. That matters beyond tidiness:
a TC[23,14] copy is refused unless the link is in Run, so a link that had merely stopped
working while still reporting Run would let a transfer be commanded into a void. When the link
drops with a transfer in flight, the DHS fails that transfer immediately rather than waiting for an
acknowledgement that may never come.
5. Packet level, and telling protocols apart
A packet opens with the destination logical address (50-12C 10.2.2) and then a protocol identifier (ECSS-E-ST-50-51C):
[0] destination logical address SPW_LA_DHS 0x20 | SPW_LA_PAYLOAD 0x21
[1] protocol identifier 0x01 RMAP | 0x02 CCSDS | 0x00 extended
[2..] protocol-specificThe protocol identifier, like the datagram’s kind octet in §2, is exactly what 50-51C exists for: a receiver must never have to guess what it’s holding from the shape of the bytes — there is no value a data octet cannot take, the mistake the CAN report tags already paid for one subsystem over. This link handles RMAP; an encapsulated CCSDS packet is a legal thing to see and is counted and named, not ignored. So is a packet addressed to the other end. “Some traffic silently disappears” is the hardest fault to find on any bus later.
6. RMAP, and why the target is safe
RMAP is what makes the link worth having. The payload doesn’t “send a file and ask the DHS to find somewhere to put it”: the DHS allocates a window in its mass memory, tells the payload the address, and the payload writes into it. No reassembly buffer exists anywhere on the receiving side.
Only the write command / write reply pair is implemented. A read or a read-modify-write is legal
RMAP that this target doesn’t offer, and gets RMAP_ST_NOT_IMPLEMENTED — the standard’s answer,
not an invention.
The write command header is sixteen octets with no reply address:
| Offset | Field |
|---|---|
| 0 | target logical address |
| 1 | protocol identifier (0x01) |
| 2 | instruction |
| 3 | destination key |
| 4 | initiator logical address |
| 5–6 | transaction identifier |
| 7 | extended address |
| 8–11 | address |
| 12–14 | data length (three octets, RMAP’s own width) |
| 15 | header CRC, over octets 0–14 |
then the data field, then one octet of data CRC. The reply is eight octets.
Two CRCs, separately. The RMAP CRC is CRC-8, polynomial x⁸+x²+x+1, LSB-first, initial value zero (50-52C Annex A) — not the CRC-16-CCITT the rest of the platform uses for PUS. Neither can be shared, because each is fixed by its own standard. Header and data are covered separately because the failures differ: a header CRC failure means the target can’t trust even the length field, so it discards silently and does not reply — the reply address would have come out of the same unverified octets. A data CRC failure is answerable, because the header that said where to reply was itself sound.
No verify-before-write. That option requires the target to buffer the whole data field before committing it, and the point of writing into a mass memory window is that the mass memory is the buffer. Integrity rides on the data CRC, checked before a single octet is committed.
The bounds check
This is what makes exposing an RMAP target safe at all: a target that trusts the address it’s handed is a remote arbitrary-write primitive.
The DHS exposes exactly one region: [SPW_SSMM_BASE, SPW_SSMM_BASE + SSMM_FILE_BYTES), the
mass memory file partition and nothing else. There is no address in the protocol that reaches the
packet store, the data pool, a task stack or any code. Both ends of every write are checked, and
the arithmetic cannot wrap — the base is compared before the length is added to it. An address
outside the window is answered with an RMAP error, counted in SPW_CRC_ERR’s sibling counters, and
raises EVT_SPW_REJECT.
The base is deliberately non-zero (0x40000000) so a zeroed or uninitialised address field is out
of range rather than a write to the first sector.
7. The mass memory: two stores, not one
The DHS’s mass memory unit now holds two things, and they must not be merged:
Packet store (xSsmm) | File partition (ucSsmmFile) | |
|---|---|---|
| Holds | generated PUS TM packets | science files |
| Size | 128 slots × 256 octets | 128 KiB, 1 KiB sectors, 16 slots |
| Full | overwrites the oldest | refuses |
| Written by | every telemetry path | the RMAP target only |
| Read by | the downlink task | service 23 and service 13 |
ECSS draws the same line — packet storage and file management are different services — and the
retention policies are opposites. Overwriting the oldest packet is correct, because fresh telemetry
is worth more than stale. Overwriting the oldest file would silently discard an observation the
ground commanded and paid for; that decision belongs to the ground, and TC[23,2] is how it’s
made.
The partition’s mutex xSsmmFileMutex is a fourth leaf on exactly the terms
dhs.md sets for the other three: it never nests with xSsmmMutex, xVerifMutex,
xSchedMutex or the pool’s lock, so every path that reports a file snapshots under it, releases,
and only then builds a packet.
8. A transfer, end to end
ground TC[23,14] copy PAYLOAD/PLD00001 -> SSMM
|
DHS allocate a 16 KiB window in the file partition
xXfer = ARMED, EVT_XFER_START
CAN: { CMD_XFER_GRANT, file_id[2], base[3], maxlen[2] } -> payload
prvVerifTrack(request, PAYLOAD, CMD_XFER_GRANT)
|
payload storage task: snapshot the file record, release the flash lock
for each block: RMAP write at SPW_SSMM_BASE + base + offset
spend FCT credit, obey the rate shaper
|
DHS spwrx: validate target LA, key, header CRC, data CRC, bounds
write into ucSsmmFile, reply, xXfer = BUSY, advance received
|
payload CAN: { CMD_XFER_GRANT, status } two octets, never more
|
DHS commit the file record, shrink the reservation to the delivered length
EVT_XFER_DONE, and the parked request completes as TM[1,7]Four things in that sequence are easy to get wrong:
- The grant is a window, not a size — eight octets exactly,
{ opcode, file_id[2], base[3], maxlen[2] }. The DHS is never told how long a payload file is (the store announcement carries an identifier and nothing else), so it grants a fixed 16 KiB window and the payload refuses the grant itself if its file won’t fit. The reservation shrinks to the delivered length on completion. - The completion ack is two octets. Exactly the discipline
CMD_DOWNLINK_FILEalready required (payload.md§11): an ack whose length carries meaning is one wire change away from meaning something else. It’s what completes the transfer —prvVerifTrackparks the request, so this is verified through the machinery every other routed command uses, with no second mechanism and no ground timeout special-cased for this service. - A failed transfer releases its reservation. Otherwise a slot stays half-written and visible to service 23, offering the ground a file that’s half an image.
The payload runs the push on its storage task, the same task as the CAN readout, for the reason
given in payload.md §7.
9. Modelled throughput, and what it is honest about
[spw] link_rate_bps (default 100 000 000) shapes the transmit path: the sender accumulates a
per-tick budget of rate / 8 / configTICK_RATE_HZ octets and yields when it’s exhausted.
The shaper’s resolution is one tick. At 100 Mbps that’s 125 000 octets per tick, so an 8 KiB
image clears inside a single tick and the modelled transfer is effectively instantaneous. Set
link_rate_bps = 1000000 to watch one take measurable time: at 1 Mbps the budget is 1250 octets a
tick, and an 8 KiB image is eight RMAP writes of 16 + spw_rmap_bytes + 1 = 1041 octets each, so
the shaper places roughly one write per tick and the transfer runs to tens of milliseconds rather
than clearing inside one.
SPW_LOAD_PCT is the busiest of the last ten one-second windows, not an instantaneous figure.
A plain average would read zero in every window except the one a transfer happened in, and the
ground samples this at housekeeping cadence — an operator asking “is the mission data link busy”
would be told “no” throughout the transfer. Ten windows because [dhs] hk_tm_period_ms is 5000, so
a burst survives long enough to appear in at least one report and usually two.
The rate is a board strap in [spw], not on-board database content. It describes the harness;
both ends read the same key at boot, so nothing can retune one of them into disagreeing with the
other about how fast the wire is — which a distributed parameter could do, one subsystem at a time.
10. Service 23, file management
Every subtype is standard ECSS-E-ST-70-41C, so a ground segment that already knows service 23 needs no mission-specific decoder to drive this link.
| Direction | Subtype | Meaning |
|---|---|---|
| TC | 23,1 | create a file (reserve space in the SSMM) |
| TC | 23,2 | delete a file |
| TC | 23,3 | report the attributes of a file |
| TM | 23,4 | file attribute report |
| TC | 23,12 | summary-report the content of a repository |
| TM | 23,13 | repository content summary report |
| TC | 23,14 | copy a file |
Not implemented: 23,5/6 lock and unlock, 23,7/8 find, 23,9/10/11 directory operations, 23,15 move, 23,16 suspend. A telecommand carrying one of those is rejected as an illegal subtype rather than silently reinterpreted, the line every other service in this platform draws.
Service 23 is DHS-local, like services 20 and 11: a file name alone is twelve octets, so it couldn’t ride the eight-octet CAN-command translation path even if the partition lived elsewhere. The one telecommand that reaches the bus, 23,14, builds its own grant frame.
Repositories
What the mission defines, rather than the standard, is the repository namespace: the set of stores a spacecraft has isn’t something a standard can enumerate.
| Id | Name | What it is |
|---|---|---|
| 0 | PAYLOAD | the imager card’s flash. The DHS can list it and copy out of it, and cannot read it |
| 1 | SSMM | the DHS’s mass memory file partition; the RMAP target |
| 2 | GROUND | not a store: copying into it is how a file is downlinked |
Naming the downlink as a repository means “send this file to the ground” is a copy like any other rather than a bespoke telecommand. Exactly two routes exist:
PAYLOAD -> SSMM— the mission data link. One-way because the payload is RMAP’s initiator and the DHS its target: there is no direction in which the DHS could push into the payload’s flash.SSMM -> GROUND— the service-13 downlink, through the sameprvDownlinkFilea file reassembled off the CAN bus already uses. It doesn’t care how the octets arrived.
Anything else is refused INVALID_PARAMS at acceptance, so the ground learns immediately that it
asked for a transfer no wire on this spacecraft can carry.
What the DHS knows about the payload’s repository
Only the file identifiers it’s heard in the payload’s five-octet store announcement. There is no catalog protocol and no new payload housekeeping frame — a second payload HK frame would trip the one-packet-per-subsystem rule for a fact the DHS already has.
The consequence is visible in telemetry and is deliberate: TM[23,4] and TM[23,13] report a
length and creation time of zero for a file in the PAYLOAD repository. That’s not a
zero-length file, it’s “the DHS was never told”. Reporting a guess would be worse.
11. Telemetry and events
Ten read-only data-pool parameters, all in the DHS’s 0x01xx band and all carried in the DHS’s own
housekeeping packet (TM_DHS_Platform_HK, now 66 octets):
| Identifier | Mnemonic | Meaning |
|---|---|---|
0x014B | SPW_LINK | link state, ErrorReset…Run |
0x014C | SPW_RX_BYTES | N-chars received: the science data volume |
0x014D | SPW_LOAD_PCT | bus load, peak-held (§9) |
0x014E | SPW_CRC_ERR | header and data CRC failures |
0x014F | SPW_EEP | packets terminated by an error EOP |
0x0150 | XFER_STATE | idle / armed / busy / complete / failed |
0x0151 | XFER_FILE | file identifier in flight |
0x0152 | XFER_BYTES | octets received this transfer |
0x0153 | SSMM_FILE_USED | file partition usage, KiB |
0x0154 | SSMM_FILE_CNT | files held |
They’re DHS-band on purpose. The link’s far end is the payload, but every one of these is
something the DHS observes at its own end of the cable or in its own store, so the ground has
exactly one source for each. The payload’s transmit-side counters — octets pushed, credit stalls —
are not telemetered at all, for want of a wire to carry them: a second payload frame would split
its housekeeping in two. They appear in the payload’s log at info.
Events, all in include/events.h: EVT_SPW_LINK_UP 0x0019,
EVT_SPW_LINK_DOWN 0x001A, EVT_XFER_START 0x001B, EVT_XFER_DONE 0x001C,
EVT_XFER_FAIL 0x001D, EVT_SPW_REJECT 0x001E, EVT_FILE_DELETED 0x001F. The two link
events are raised in one place, prvSpwSyncStatus, on the orchestrate cycle — one function that
notices a change, rather than a check scattered through whichever paths happen to look.
12. Driving it from the ground
egse_tc DHS TIME NOW # service 23 timestamps want a synced clock
egse_tc DHS MODE SCIENCE # the imager does not expose in SAFE
egse_tc DHS CAPTURE # 8 KiB frame -> the payload's flash
egse_tc DHS FILE LIST PAYLOAD # TM[23,13]: the announced identifiers
egse_tc DHS FILE COPY PAYLOAD PLD00001 SSMM # the SpaceWire transfer
egse_tc DHS FILE INFO SSMM PLD00001 # TM[23,4]: length 8192
egse_tc DHS FILE COPY SSMM PLD00001 GROUND # downlink as TM[13,1] + 41x TM[13,2]
egse_tc DHS FILE DELETE SSMM PLD00001 # free the sectorsAllow a second or two between the mode change and the capture: the payload refuses a capture until it has adopted SCIENCE, and it’s told over the bus.
The ground link, not SpaceWire, is now the bottleneck, and the same is true of the fallback route. Three transports carry the same 8 KiB image:
| Route | Framing | Cost |
|---|---|---|
| SpaceWire RMAP | 8 writes of 1041 octets | clears inside one 10 ms tick at the shipped 100 Mbps |
| CAN readout, payload→DHS | 1 FF + 1171 CFs, vTaskDelay(1) every 8 | ≈ 146 ticks, about 1.5 s |
| DHS→TTC→ground | 41 × TM[13,2], each ≈ 32 CAN_FUNC_PKT segments paced the same way | ≈ 1300 frames, of the same order again |
Three orders of magnitude between the first row and the other two is the whole argument for the second bus — and the fact that the last row still costs that much, whichever route the octets took to reach the mass memory, is the honest result of putting a fast bus in front of a slow one.
13. Fidelity envelope
Not modelled, and none of it changes what the flight software above the link sees — the line this
platform’s other models draw too (dyn_engine.md):
- the character-level encoding (data-strobe, parity, 4b/10b — there are no bits on a wire here, only UDP datagrams), and disconnect/parity detection at character granularity (§2);
- time codes, routing switches and path addressing (§1, §5);
- RMAP reads and read-modify-writes, answered
RMAP_ST_NOT_IMPLEMENTED(§6); - the standard’s microsecond state timings, which are ticks (§4).