Skip to content

DHS — the Data Handling Sub-System

Reference documentation for nodes/dhs_node.c, the on-board data handling sub-system. See index.md for the platform overview, ttc.md for the link layer it hands packets to, pus.md for the wire format, and obdb.md for the parameter model.

CAN address 0x1, PUS APID 0x001. One binary, build/bin/dhs_node.


1. Role and boundaries

The DHS is the CAN↔PUS boundary. Inside the spacecraft everything is an 8-byte CAN frame; the ground link carries CCSDS Space Packets with ECSS PUS-C secondary headers. The DHS is the only node that translates between the two.

It owns seven things:

ResponsibilitySummary
PacketisationBuilds every downlink TM packet, attributing each to its producing subsystem by APID
Telecommand routingDecodes uplinked PUS TC, routes CAN commands, answers service-1 verification
Mass memory (SSMM)A static ring of generated packets held for downlink, a file partition for science data, and an event log
The mission data linkThe RMAP target end of the SpaceWire link, and PUS service 23 as its control plane
Clock masteringOwns on-board time and broadcasts it to every other node
The data poolOne addressable, typed, access-controlled view of on-board state, exposed via PUS service 20
The on-board databaseHolds every other subsystem’s mission parameters and distributes them over the bus

Not the DHS’s: RF, modulation, channel coding and CCSDS transfer frames belong to TTC, the link layer. The DHS hands TTC whole PUS packets, segmented over CAN_FUNC_PKT; TTC buffers them across link outages and streams them to the EGSE. CAN never leaves the spacecraft.

“OBC” refers only to the redundant computer hosting the DHS — see §12.


2. Boot sequence

Everything that reads a file happens in main() (:4929) before vTaskStartScheduler(), because a task blocked in a syscall starves the Posix port.

logInit("DHS")
  └─ prvLoadConfig                    :6162   also reads [spw] link_rate_bps, a board strap
       └─ prvReloadTunables           :6049   [dhs] section → xSys, xHkTable, boot mode
  └─ prvInitDataPool                  :6190
       ├─ prvObdbBuild                :2557   materialise DP_CFG_CATALOG, seed ROM defaults
       ├─ prvObdbLoad(obdb_file)      :2747   restore the MRAM image "MOB1"…
       │    └─ prvObdbProgramFromFile :2612   …or, if absent/corrupt, program from minscs.conf
       ├─ prvRegisterPoolParams       :3178   bind 99 static params + every database row
       ├─ dpNvramLoad / dpNvramRestore        apply the operator delta over the baseline
       └─ dpStart                             from here the pool is shared between tasks
  └─ xSemaphoreCreateMutex × 4                xSsmmMutex, xVerifMutex, xSchedMutex, xSsmmFileMutex
  └─ canInit(CAN_ADDR_DHS)                    creates the "canrx" driver task
  └─ spwInit(SPW_LA_DHS, listen)              creates the "spwrx" driver task; binds :20100
  └─ xTaskCreate × 5                          disp, orch, hkpoll, fdir, dnlink
  └─ vTaskStartScheduler

prvInitDataPool’s ordering is the whole design in miniature: ROM → MRAM → NVRAM, each layer applied over the last, so the parameter file is the baseline and the non-volatile mirror is the operator’s override. See §10.


3. Tasks

main() creates five tasks; canInit() creates a sixth and spwInit() a seventh. All seven are pthreads under the Posix port, and each costs ~128 KiB of the 4 MiB FreeRTOS heap (stack depth is counted in words), so the DHS’s task budget is ≈ 896 KiB.

TaskPrioritySourceRole
canrxPRIO_CAN_RX (5)common/can_drv.cBounded recvfrom into the RX queue; yields every CAN_RX_BATCH frames
dispPRIO_APP (3):4566CAN RX demultiplex — the only consumer of canRxQueue()
orchPRIO_APP (3):4394The decision loop: autonomy, scheduling, periodic telemetry, file writes
fdirPRIO_APP (3):4253FDIR: the heartbeat watchdog and the bus undervoltage monitor
hkpollPRIO_TLM (2):4200Polls subsystems for housekeeping per xHkTable
dnlinkPRIO_BULK (1):4679Drains the SSMM to TTC over CAN_FUNC_PKT
spwrxPRIO_CAN_RX (5)common/spw_drv.cThe SpaceWire link: bounded recvfrom, the initialisation state machine, and the RMAP target callback that writes into the mass memory file partition

prvDispatchTask — CAN receive demultiplex

Blocks on xQueueReceive (a FreeRTOS primitive, so blocking is safe) and switches on the frame’s function code:

FunctionHandling
CAN_FUNC_HK (0x2)prvIngestHk — parse into the platform mirror, then packetise as TM(3,25)
CAN_FUNC_SCI (0x3)Packetised as TM(3,25) too; the APID says which instrument
CAN_FUNC_XFER_FF (0x4)prvHandleFirstFrame — open a payload file reassembly
CAN_FUNC_XFER_CF (0x5)prvHandleConsecutiveFrame — append, and downlink on completion
CAN_FUNC_PKT (0x7)prvHandlePktSegment — reassemble an uplinked PUS TC from TTC
CAN_FUNC_CMD (0x0)Only CMD_LOW_POWER is expected inbound (the EPS asserting/clearing)
CAN_FUNC_ACK (0x1)Payload file-stored notice, or an OBDB Ready, or a tracked TC completion

The ACK branch is ordered deliberately: CMD_CFG_END acks are taken by prvObdbAck before prvVerifMatchAck, or they’d be logged as acks for a telecommand nobody sent.

prvOrchestrateTask — the decision loop

Runs every orchestrate_tick_ms (default 1000). ORCH_TICK is deliberately read-only in the data pool — it is the divisor of every cadence below, so a wild value from the ground would break the loop rather than merely retune it.

StepCadence
Low-power safing / recovery (§8)every tick
Thermal limit checkevery tick
prvObdbSchedule — database distributionevery tick, after ulTick >= 2
Mode announcement, once, on database convergenceone-shot
prvVerifSweep — retire timed-out telecommandsevery tick
prvSchedRelease — release due scheduled activitiesevery tick
prvBroadcastTimetime_sync_period_ms / orchestrate_tick_ms
prvEmitOwnHkhk_tm_period_ms / orchestrate_tick_ms
prvEmitAdcsHk / prvEmitEpsHk — one housekeeping report eachthat subsystem’s xHkTable period_ms / orchestrate_tick_ms
dpNvramFlush + prvObdbFlushnvram_flush_period_ms / orchestrate_tick_ms, and only if dirty

The first two ticks skip distribution because the hub learns routes at runtime and silently drops a frame to an unlearned address; prvHkPollTask delays its first pass 1500 ms for the same reason.

prvHkPollTask is covered in §6.

prvDownlinkTask — mass memory drain

Takes the oldest stored packet, segments it to TTC with canPktSend, and retires the slot. It is the sole writer of CAN_FUNC_PKT toward TTC, so segments from different packets never interleave on the bus. Segmenting releases the SSMM lock for several ticks, so the task records which slot index it took and only retires that slot if read has not moved underneath it — a full ring can overwrite and re-advance read in the meantime.


4. Telecommand handling

The two-stage model

The DHS implements the ECSS request-verification model as two functions the uplink path calls back to back:

  1. AcceptanceprvAcceptTc (:3979): is the destination routable, the service implemented, the subtype legal, the parameters valid? Answers TM[1,1] or TM[1,2] (+ error code). Pure — validates and translates to a CAN command, emits nothing.
  2. ExecutionprvExecuteTc (:4069): carries it out and answers completion, TM[1,7] or TM[1,8] (+ error code).

prvHandleTc (:4146) is just those two with the acceptance report between them; they stay separate because the command schedule calls them at two different times with a third report in between.

Every report opens with a 4-octet request identifier echoing the TC’s primary header. Failure error codes: ILLEGAL_SERVICE, INVALID_PARAMS, INVALID_STATE, EXEC_FAILED, TIMEOUT, SCHED_FULL, SCHED_PAST.

Destination

The target subsystem is the TC’s APID (low nibble, :4146), validated against the five node addresses plus CAN_ADDR_BROADCAST (prvIsRoutableAddr, :2989). An invalid target is rejected INVALID_PARAMS rather than sent to the hub, which would silently drop it and leave the request to expire into a misleading TIMEOUT.

Services accepted

Seven. Anything else is rejected ILLEGAL_SERVICE.

ServiceSubtype acceptedTranslationExecuted
17 test1 connection testCMD_PINGon the target
3 housekeeping27 one-shot reportCMD_REQ_HKon the target
8 function1 perform, 2 perform-HPapp data is the CAN command (≤ 8 octets)on the target
9 time128 set clocklocally
11 scheduling1, 2, 3, 4, 5, 16locally
20 parameters1, 3, 133, 134locally
23 file management1, 2, 3, 12, 14locally (14 also puts a CMD_XFER_GRANT on the bus)

Subtypes the service emits (17,2; 3,25; 11,17; 20,2; 23,4; 23,13) are also rejected ILLEGAL_SERVICE. Service 8 subtype 2 (PUS_ST_FUNC_PERFORM_HP) is the high-priority class TTC hands straight to the addressed subsystem, so one only reaches the DHS if addressed to the DHS itself.

Three execution paths

PathWhenCompletion
Localservices 9, 11 and 20; any command with dst == CAN_ADDR_DHSsynchronous — prvVerifCompleteNow
Dispatch-is-completionservice 3 polls, and any broadcast (dst == 0xF)synchronous, on send
Parkedeverything else routed to a subsystemasynchronous — on the subsystem’s CAN ack, or TIMEOUT

The middle row exists because an HK poll’s result comes back on the housekeeping channel, not a command ack, and a broadcast ack always carries the real sender’s address, never 0xF.

The pending table

xVerif (:390) holds up to VERIF_PENDING_SLOTS (16) parked requests — { request id, destination, CAN command byte, issue tick } — under xVerifMutex. An ack matches on (dst == src) && (cmd == data[0]); anything still active after verif_timeout_ms becomes TM[1,8] TIMEOUT. Static, like the SSMM.

Commands the DHS executes itself

prvExecDhsCommand (:2900), reached when the TC’s APID is the DHS:

OpcodeAction
CMD_PING (0x01)Log only; completion is the answer
CMD_REQ_HK (0x02)prvEmitOwnHk
CMD_SET_MODE (0x03)Platform mode → broadcast to the whole bus. Refused INVALID_STATE if low power is asserted and the mode is not SAFE
CMD_CAPTURE (0x10)Forward to PAYLOAD
CMD_DOWNLINK_FILE (0x11)Forward the file id to PAYLOAD
CMD_FLASH_STATUS (0x12)Forward to PAYLOAD
CMD_DUMP_EVENTS (0x21)prvDumpEvents — replay the whole event log
CMD_PARAM_DUMP (0x25)prvParamDumpAll — report the whole pool as chunks
CMD_CFG_PUSH (0x29)Invalidate one or all peers, forcing re-distribution

A platform command addressed to the DHS fans out to the bus (prvSetMode broadcasts); one to a subsystem routes straight there.

Time-based scheduling (service 11)

A queue of telecommands lodged against an absolute on-board time, so an activity can be commanded in one contact and executed with nobody listening. A TC[11,4] carries a release time and a whole embedded telecommand; the DHS verifies and stores it, and the orchestrate loop releases it when the clock reaches that time.

SubtypeVerbBody
11,1 / 11,2arm / hold releases (the queue is kept either way)
11,3discard every entry
11,4insertN(2) then N × { coarse(4) fine(2) len(2) tc[len] }
11,5delete by request idN(2) then N × request id(4)
11,1611,17report the queue— → chunk(1) M(2) then M × 16-octet record

xSched (:420) is a static array of SCHED_SLOTS (16) × up to SCHED_TC_BYTES (96) octets, kept sorted by release time.

  • The embedded telecommand is deep-copied — its app_data points into the uplink reassembly buffer, which the next telecommand overwrites.
  • Acceptance runs twice: insert (prvSchedValidate, :3067) only checks well-formed and routable; release (prvSchedRelease, :3408) re-runs the full prvAcceptTc, since legal-when-lodged may not be legal now.
  • No schedule inside the schedule — a TC[11,4] embedding a service-11 command is rejected ILLEGAL_SERVICE.
  • Releases gate on obtIsSynced(): an unsynced clock makes release times meaningless, so a reboot leaves a restored schedule dormant until TC[9,128] rather than dumping it onto the bus.

The release loop runs right after prvVerifSweep: TM[1,3] against the original request id, then prvAcceptTc followed by prvExecuteTc with that id, closing the lifecycle the ground opened. An activity that cannot start produces TM[1,4] plus EVT_SCHED_FAILED and the loop continues (ECSS-E-ST-40C error isolation) rather than stalling on a malformed entry.

The safe-guard mirror

prvDhsReboot wipes the queue like the pending-verification table, but its statisticsSTAT_SCHED_REL, STAT_SCHED_FAIL, STAT_SCHED_DROP — are read-only with DP_ACC_NVRAM, so dpRebindVolatile restores them and they survive an OBC switch. SCHED_COUNT does not persist (a depth is a fact about now); SCHED_ENABLED does, so a disarmed schedule doesn’t come back armed. Read-only plus NVRAM is a combination nothing else uses — it gates on DP_ACC_NVRAM alone, never DP_ACC_WRITE, so it’s persisted by the DHS and not forgeable by a TC[20,3].

Uplink reassembly

TTC forwards whole PUS TC packets segmented under CAN_FUNC_PKT: a first segment (start marker 0x80 + 2-octet length + 5 octets) then continuations (7-bit rolling sequence + 7 octets). canPktFeed reassembles into xTcReasm; a gap, overflow or continuation with no start drops the whole packet, with no retransmission. On a complete packet, prvHandlePktSegment (:4178) decodes it, checks the CRC and TC type, and calls prvHandleTc.


5. Telemetry generated

Every packet is built by prvEmitTmLocked (:486), which stamps the current on-board time, increments the per-APID sequence and message counters, and stores the result in the SSMM. An oversized packet is dropped, not truncated.

Provenance: the APID names the producing application, not the DHS — prvIngestHk uses pusApidFromAddr(CAN_ID_SRC(...)), the CAN frame’s source address, so an ADCS housekeeping frame becomes an APID-3 packet and a payload science frame an APID-5 packet.

The packet shapes

PacketAPIDSource
(3,25) housekeepingproducing subsystemprvIngestHk :685 — per inbound HK frame; TTC/PAYLOAD only
(3,25) ADCS report0x003prvEmitAdcsHk :1026, 33 octets, see §6
(3,25) EPS report0x004prvEmitEpsHk :1071, 30 octets, see §6
(3,25) scienceproducing subsystemprvDispatchTask :4566 — shares the HK path
(3,25) DHS platform report0x001prvEmitOwnHk :903, 66 octets, see below
(5,1..4) event report0x001prvRaiseEvent :541, prvDumpEvents :578
(1,1)/(1,2)/(1,7)/(1,8) verification0x001prvVerifEmit :1667
(1,3)/(1,4) start of execution0x001prvVerifStart :1692 — scheduled releases only
(13,1)/(13,2) large data0x005 PAYLOADprvDownlinkFile :1124
(11,17) schedule detail0x001prvSchedEmitChunk :3222
(20,2) parameter values0x001prvParamEmitChunk :2599
(23,4) file attributes0x001prvFileEmitAttr :3532, 26 octets, see spacewire.md
(23,13) repository summary0x001prvFileEmitRepo :3575, chunked, 24-octet records

CAN_FUNC_XFER_FF/_CF do not map directly onto 13,1/13,2 — the frames are reassembled first, and only a complete, gap-free file is downlinked (:1206).

The DHS’s own housekeeping report

66 octets, prvEmitOwnHk (:903) — strictly the parameters this computer owns, the 0x01xx band and nothing else:

OffsetFieldPool id
0Platform modeSYS_MODE
1Low-power flagSYS_LOW_POWER
2Autosafe flagSYS_AUTOSAFE
3Hosting OBCSYS_OBC
4Tracked ADCS boardSYS_ADCS_CPU
5Tracked TTC boardSYS_TTC_CPU
6..9TM packets builtSTAT_TM_BUILT
10..13Telecommands acceptedSTAT_TC_OK
14..17Telecommands rejectedSTAT_TC_REJ
18..19Mass-memory depthSTAT_SSMM_CNT
20..23Mass-memory packets droppedSTAT_SSMM_DROP
24..27Verification timeoutsSTAT_VERIF_TMO
28..31Non-volatile mirror writesSTAT_NVRAM_WR
32..35On-board database epochCFG_EPOCH
36Database readiness mask (bit per peer: TTC, ADCS, EPS, PAYLOAD)CFG_READY_MSK
37..38Schedule depthSCHED_COUNT
39..42Schedule activities releasedSTAT_SCHED_REL
43..46Schedule activities failedSTAT_SCHED_FAIL
47..50Schedule activities droppedSTAT_SCHED_DROP
51Mission data link stateSPW_LINK
52Mission data link load, %SPW_LOAD_PCT
53..56Octets received over the linkSPW_RX_BYTES
57File copy stateXFER_STATE
58..61Octets received this copyXFER_BYTES
62Files held in the mass memory partitionSSMM_FILE_CNT
63..65Software version, major / minor / patchSYS_SW_VERSION

The last three octets are the build this spacecraft is running, from include/version.h, checkable by eye against mdb/minscs.xtce’s header (see mdb.md); every other field is read according to that same file. The read-write thresholds and collection table stay out — they’re database content, not measurements, the same line drawn in the ADCS and EPS reports.

Locking. xTm/xSsmm (under xSsmmMutex) and the verification/schedule statistics (other locks, none of which may nest, §13) are snapshotted into locals before the packet goes out through prvEmitTmLocked. Note the downlinked tm_built is the count before this report, since emitting it increments the counter.

Unlike the ADCS and EPS reports, a one-shot TC DHS HK produces an immediate downlinkCMD_REQ_HK addressed to the DHS calls prvEmitOwnHk synchronously, with no CAN round trip.

Timestamps: a CAN housekeeping frame already fills all 8 octets, so a 6-octet CUC timestamp cannot ride alongside the data at the producing subsystem. The DHS stamps each packet with the synchronized time at the moment it builds it — within one CAN hop of the subsystem’s sample time — baked in at generation, so telemetry waiting in TTC’s buffer through a link outage keeps its original time rather than being re-dated at flush.

The mass memory unit

Two stores with opposite retention policies:

  • xSsmm (:273), a static ring of SSMM_SLOTS (128) × SSMM_MAX_PACKET (256 B) = 32 KiB of generated packets. When full it overwrites the oldest undownlinked packet: fresh telemetry beats stale.
  • ucSsmmFile, a 128 KiB partition of science files, which refuses when full — a science file is an observation the ground commanded, so discarding one is the ground’s decision, via TC[23,2]. See §10.5.

Both sized at compile time, so neither perturbs the heap budget.


6. Housekeeping collection

Every subsystem self-emits housekeeping on its own timer; the DHS additionally polls the pollable ones as a backstop. The collection table xHkTable (:92) has one row per remote subsystem, configured by the hk_<sub>_enabled / hk_<sub>_period_ms keys in §14. enabled gates polling and downlink, but not ingest — a disabled subsystem isn’t polled or packetised to the ground, but prvIngestHk still parses each frame into the platform mirror, so autonomous safing is never blinded by a disabled row.

TTC has no on-demand HK responder (poll_cmd == 0), so it’s listed but never polled — only hk_ttc_enabled has effect. The payload is polled with CMD_FLASH_STATUS rather than CMD_REQ_HK.

The table is live: every row is registered in the data pool (HK_ADCS_ONHK_PL_PER), so a TC[20,3] retunes it with no task restart. The poll task recomputes its base wake quantum — the smallest enabled, pollable period — on every wake rather than latching it at startup, so a lowered period takes effect immediately.

What the DHS parses out of each frame

prvIngestHk (:685) writes through dpWriteF32/dpWriteI32 rather than straight to the field, so the pool stamps validity and update time — the difference between “the rate is zero” and “we have never heard from the ADCS”.

SourceTagParsed into
ADCSADCS_RPT_RATE (0xD1)ADCS_RATE_X/Y/Z (hundredths of °/s)
ADCSADCS_RPT_ATT (0xD2)ADCS_QE_X/Y/Z, and ADCS_QE_W reconstructed from the unit norm
ADCSADCS_RPT_CTRL (0xD3)ADCS_PLAT_MODE, ADCS_CTRL_MODE, ADCS_LOCKED, ADCS_FDIR, ADCS_ERR_DEG, ADCS_HB (only when dlc >= 8)
ADCSADCS_RPT_ACT (0xD4)ADCS_WHL_RPM1/2/3
ADCSADCS_RPT_SENS (0xD5)ADCS_MAG_X/Y/Z, and the validity bits ADCS_SUN_VAL, ADCS_MAG_VAL, ADCS_ST1_VAL, ADCS_ST2_VAL, ADCS_ECLIPSE
EPS(untagged)EPS_SOC_PCT, EPS_BUS_V, EPS_ARRAY_A, EPS_LOAD_A
EPSEPS_RPT_POWER (0xE1)EPS_RAIL_INSTR, EPS_RAIL_RADIO, EPS_RAIL_BUS
EPSEPS_RPT_REDUN (0xE2)SYS_OBC_PWR; a change of active OBC triggers prvDhsReboot
EPSEPS_RPT_REDUN_ADCS (0xE3)SYS_ACS_PWR; active board tracked and logged, no reboot
EPSEPS_RPT_REDUN_TTC (0xE4)SYS_TTC_PWR; active board tracked and logged, no reboot
EPSEPS_RPT_THERMAL (0xE5)EPS_TEMP_BATT, EPS_TEMP_RAD (hundredths of °C)
EPSEPS_RPT_ORBIT (0xE6)EPS_ECLIPSE, EPS_ORB_VAL, EPS_SUN_INC, EPS_BETA_DEG, EPS_FDIR
EPSEPS_RPT_PL_PWR (0xE7)EPS_PL_CAM_ON, EPS_PL_GNSS_ON, EPS_PL_MEM_ON
PAYLOADCMD_FLASH_STATUS (0x12)PL_FLASH_USED, PL_FLASH_TOTAL, PL_FLASH_FILES
TTCnot parsed; link parameters are packetised as ordinary APID-2 housekeeping

The untagged EPS frame is reached by falling through every tag test above, parsed without checking dlc. That’s safe only because each tag is tested first — a tagged report this table doesn’t list is read as a state of charge, a bus voltage and two currents, driving autonomous safing. Any new EPS report tag must be added here in the same change that puts it on the wire, since the EPS’s ground report is built from these parameters rather than relayed.

One report per subsystem

Two subsystems’ housekeeping is not relayed frame for frame. The emit gate at the tail of prvIngestHk skips them, and a generator builds one report each from the parameters above, on that subsystem’s collection-table cadence, in prvOrchestrateTask:

GeneratorAPIDOctetsReplacesField map
prvEmitAdcsHk (:1026)0x00333five tagged framesadcs.md §8
prvEmitEpsHk (:1071)0x00430eight frameseps.md §8

Each packet describes its subsystem even though this node builds it, so the field maps live there.

The rule is about frame count, not about those two subsystems — eight octets is what a CAN frame holds, so a subsystem with more to say splits it. Building the packet here rather than relaying the frames keeps the picture unskewed in time and stops frame sizes being ground-visible: one container per subsystem instead of several sized to fit a CAN frame. TTC and the payload stay on the relay path, where the frame and the picture are the same thing.

Both generators share two gates through prvHkGenReady (:987): the row’s enabled flag, and a report withheld until the subsystem has actually reported — asked of the pool, not a private flag, so a modelled reboot silences the report automatically. Consolidating required two additions to the mirror: the EPS’s rail currents, and the ADCS’s own view of the platform mode (ADCS_PLAT_MODE, not SYS_MODE, this computer’s own belief).


7. Events

The event log xEvlog (:362) is a static ring of EVLOG_SLOTS (64) occurrences. Each is logged and downlinked immediately as a PUS service-5 report at its severity subtype; TC DHS EVENTS replays the whole log oldest-first.

The identifiers are not declared in this node. They live in include/events.h as an X-macro catalog, on the same principle as DP_PID_CATALOG: the DHS takes the EVT_* constants from it and egse_tm takes evtName() from it, so flight code and ground tool cannot disagree about what 0x0020 is called; tests/test_events.c asserts the round trip for every row. That header is the list.

Severity is deliberately not in that catalog — it is the subtype the report is emitted with, chosen where the event is raised, since the same occurrence can warrant different severities in different circumstances. Severity and the two parameter octets are what this table adds. The 9-octet report body carries the occurrence time (prvPackEvent, :529), so a replayed log keeps its original timing even though the replay packets are stamped when emitted:

OffsetField
0..1Event identifier
2Severity (== the PUS subtype)
3..6On-board time of the occurrence (CUC coarse)
7..8Two event-specific parameter octets
EventSeverityp0, p1
MODE_CHANGE1 infonew mode, —
SAFING3 mediumSoC, —
POWER_RECOVER1 infoSoC, —
LOW_POWER3 mediumSoC, —
CLOCK_SET1 info—, —
TC_REJECTED2 lowservice, subtype
FILE_STORED1 infofile id hi, lo
FILE_DOWNLINK1 infofile id hi, lo
OBC_SWITCH3 mediumold OBC, new OBC
ACS_SWITCH3 mediumold board, new board
TTC_SWITCH3 mediumold board, new board
PARAM_SET1 infocount, —
PARAM_DEFINE1 infonew pid hi, lo
PARAM_DELETE1 infocount, —
PARAM_NVRAM1 inforestored count, —
THERM_LIMIT3 mediumbattery °C, limit °C
CFG_DISTRIB1 infosubsystem, row count
CFG_READY1 infosubsystem, applied count
CFG_FAILED2 lowsubsystem, last NAK
CFG_PROGRAMreserved; declared, not yet raised
SCHED_INSERT1 infoqueue depth, inserted count
SCHED_RELEASE1 inforeleased service, subtype
SCHED_FAILED2 lowerror code, service
SCHED_FULL2 lowqueue depth, refused count
SPW_LINK_UP1 infolink state, —
SPW_LINK_DOWN3 mediumlink state, —
XFER_START1 infofile id hi, lo
XFER_DONE1 infofile id hi, lo
XFER_FAIL3 mediumfile id lo, ack status
SPW_REJECT3 mediumoffending address, high octets
FILE_DELETED1 infofile id hi, lo
FDIR_ADCS_TMO1 infofrozen heartbeat, miss count
FDIR_BUS_UV4 highbus volts, limit volts

8. Autonomy

Four autonomous behaviours, all the DHS’s own judgement rather than relayed commands. Two run in the decision loop; the two FDIR monitors run in their own task and hand their recovery back to the decision loop to actuate.

Low-power safing

The EPS raises CMD_LOW_POWER ({ CMD_LOW_POWER, SoC, asserted }) below its soc_low_pct and clears it above soc_recover_pct — the third octet lets the DHS learn the battery recovered, since it holds no power model of its own. The DHS treats it as an edge, so a hysteretic EPS re-announcing a held state doesn’t fill the event log.

While asserted the DHS safes the platform and refuses every non-SAFE mode telecommand with INVALID_STATE. Recovery is a separate judgement: the DHS only resumes NOMINAL once soc_pct > soc_resume_pct. Only an autonomous SAFE auto-recovers, gated by xPlat.autosafe — a commanded SAFE, or the boot-time SAFE after a reboot, is held until the ground commands otherwise.

Thermal limit

The battery temperature is compared against THERM_LIMIT_HI read from the pool, so a TC[20,3] changes what the spacecraft does, not just a number it reports back. The alarm is latched, raising one EVT_THERM_LIMIT rather than one per tick, and clears when the temperature falls back.

FDIR — detection and recovery

prvFdirTask :4253 runs two monitors every fdir_period_ms (2 s). The task detects and isolates; the decision loop recovers, because both recovery actions touch state the FDIR task must not — prvObdbInvalidate writes the lock-free xObdbPeer[], and prvSetMode may race the low-power arm’s own bus command — so each recovery is latched in xFdir and actuated in prvOrchestrateTask, the same hand-off shape the ADCS uses for its reboot.

xFdir needs no mutex: every word has exactly one writer. adcs_reset_req is a monotonic counter (a set/clear flag could lose an edge to the consumer clearing it); uv_safe is level-written, only ever 1, so a lost write is simply re-issued next tick.

Both monitors arm on validity, not on a timer — a parameter never written is not one that stopped being written, which keeps the watchdog quiet through boot and, since dpRebindVolatile returns every read-only mirror to invalid, disarms both across a modelled reboot with no reboot-specific code.

Subsystem heartbeat — the ADCS watchdog

The ADCS control task advances a counter once per cycle and the housekeeping task echoes it in the eighth octet of ADCS_RPT_CTRL, mirrored as ADCS_HB (0x0341) — counting in the control task while a different task transmits is what lets a stalled control loop be distinguished from a merely quiet one.

If the value doesn’t change for FDIR_ADCS_TICKS consecutive samples the subsystem is identified as COMM_TIMEOUT: EVT_FDIR_ADCS_TMO fires once per outage (not per tick); every ADCS mirror identifier is marked invalid, which stops prvEmitAdcsHk building a fictional packet from stale values (ADCS_HB itself stays valid — the frozen count is the evidence); and a warm reset is requested, which the decision loop turns into prvObdbInvalidate(CAN_ADDR_ADCS), re-pushing the config block on the same tick.

Recovery self-heals with no recovery code: every write from prvIngestHk re-stamps DP_ST_VALID, so the first report after the ADCS returns is coherent rather than a packet of zeroes. A TC[20,133] derived parameter with an ADCS source flips to DP_ST_STALE while the trip stands.

Bus undervoltage

EPS_BUS_V is compared against FDIR_BUS_UV_V read from the pool, the same contract as the thermal check. Below it for FDIR_UV_TICKS consecutive ticks the DHS raises EVT_FDIR_BUS_UV at severity 4 (the platform’s first use of TM[5,4]) and latches a safing request the decision loop turns into MODE_SAFE.

This is a backstop below the EPS’s SoC-based low-power alert: the shipped cell spans 26.0–29.4 V and the EPS asserts low power at 30 % SoC, so the 26.5 V default is only reached if that alert already failed.

The latch is sticky — only a ground mode command lifts it. The auto-recovery arm carries && !xFdir.uv_safe :4411: since prvSetMode no-ops when the mode already matches, without the guard an undervoltage arriving during a low-power SAFE would leave no trace, and the spacecraft would resume NOMINAL with the bus still under its limit. While the latch stands, low-power safing cannot auto-recover until the ground clears it, and clearing it while the fault persists re-trips it three ticks later.

Observability and control

IdentifierAccessMeaning
FDIR_ENABLED 0x0117RW, persistedMaster switch. Disabling holds the counters where they are rather than clearing them, so an operator silencing a monitor can still see what it had reached
FDIR_ADCS_TICKS 0x0118RW, persistedUnchanged heartbeats before COMM_TIMEOUT, 1..60
FDIR_BUS_UV_V 0x0119RW, persistedBus undervoltage limit in volts, 0..40
FDIR_UV_TICKS 0x011ARW, persistedConsecutive ticks under the limit before safing, 1..60
FDIR_STATUS 0x0155RO0x01 ADCS timeout, 0x02 bus undervoltage, 0x04 the ADCS has never reported so the watchdog is idle
FDIR_ADCS_MISS 0x0156ROConsecutive unchanged heartbeats
FDIR_UV_MISS 0x0157ROConsecutive ticks under the limit

The four thresholds are ordinary read-write pool parameters, so TC[20,3] retunes them and the non-volatile mirror carries the change across a reboot. They are deliberately not DP_CFG_CATALOG rows: the on-board database is what the DHS distributes, and these govern a decision the DHS makes at its own end. The miss counters are two numbers rather than one OK/TRIPPED enum so an operator can see how close the monitor runs to its threshold.


9. Parameter management — the data pool

The DHS’s state would otherwise be scattered across three private tables: the platform mirror, the tunables loaded from the parameter file, and the housekeeping-collection table. The data pool gives all of it one addressable, typed, access-controlled face; PUS service 20 is the ground’s window.

The pool binds; it does not copy

Registration points an entry at the C field that already holds the value — &xPlat.soc_pct, &xSys.verif_timeout_ms, &xHkTable[i].enabled — so a ground write lands in the field flight code reads. obdb.md §1 is the model. To add a parameter: one DP_PID_CATALOG row in include/datapool.h, one prvPoolReg call in prvRegisterPoolParams (:2404). Do not add a config key per parameter — that’s exactly what the binding exists to avoid.

Identifiers

A 16-bit identifier whose high octet is the producing subsystem’s CAN address, so provenance reads at a glance the way an APID does: 0x01xx DHS, 0x02xx TTC, 0x03xx ADCS, 0x04xx EPS, 0x05xx PAYLOAD, 0x0Fxx the ground-defined block.

GroupRangeAccess
Platform state mirror0x01010x010Aread-only
DHS thresholds and cadences0x01100x0116read-write, ranged, persisted (ORCH_TICK read-only)
FDIR thresholds0x01170x011Aread-write, ranged, persisted
HK collection table0x01200x0127read-write, ranged, persisted
Database status0x01300x0131read-only
Workload and schedule statistics0x01400x014Aread-only (the three schedule counters also persisted)
Mission data link and the mass memory file partition0x014B0x0154read-only
FDIR working state0x01550x0157read-only
Remote subsystem measurements0x03xx, 0x04xx, 0x05xx (low octet < 0x80)read-only
The on-board databaselow octet ≥ 0x80 in 0x02xx0x05xxread-write, ranged, persisted
Ground-defined0x0F000x0F1Fdynamic

The measurement/database split is DP_PID_IS_CFG, and 0x80 is the current threshold — obdb.md §2 owns that rule and why moving it again invalidates a stored MRAM image.

Read-only entries start invalid and become valid only when a subsystem actually reports, so the ground can always tell a real measurement from a subsystem that has never spoken — except the DHS’s own facts (prvMarkOwnFactsValid, :2371), known from boot.

The wire

TM[20,2] opens with a chunk-control octet and a 2-octet record count, then that many 8-octet records: identifier (2), type (1), status (1), value (4, right-aligned big-endian) — fixed-width for cheap chunk arithmetic, at the cost of a few wasted octets on a small parameter. A request for more parameters than one packet holds is answered in several chunks rather than truncated, consistent with §5’s rule that an oversized packet is dropped rather than sent short.

SubtypeDirectionBody
20,1 report valuesTCN(2) then N × identifier(2), max 30
20,2 values reportTMchunk(1), M(2) then M × record(8), max 29
20,3 set valuesTCN(2) then N × record(8), reserved octet zero
20,133 defineTCcount(1)=1, id(2), type(1), op(1), nsrc(1), src0(2), src1(2), name(16)
20,134 deleteTCN(2) then N × identifier(2)

Query versus mutation

TC[20,1] is a query: an unknown identifier is answered with an UNDEFINED record rather than rejected, and the reply’s record count matches the request’s so correlation stays positional.

TC[20,3] is the opposite: every record is validated at acceptance (exists, write access, exact type, in range), and any failure rejects the whole command with TM[1,2] INVALID_PARAMS and nothing written — partial application is exactly what the two-stage model prevents. On success the new values are echoed back as a TM[20,2], confirming what the spacecraft holds.

Ground-defined parameters

TC[20,133] defines a parameter in the dynamic block, free-standing or derived from one or two existing parameters through a typed operator; TC[20,134] deletes one. The arena, operators and derivation rule are obdb.md §4. Define and delete sit at subtypes 133/134 since ECSS-E-ST-70-41C reserves 1..127 for its own 20,1/2/3.

TC DHS PARAMS (CMD_PARAM_DUMP) reports the whole catalog, static and ground-defined, as a run of chunks — the parameter equivalent of an event-log replay.

The non-volatile mirror: parameters marked persistable are mirrored outside everything a reboot wipes, so an operator’s retuning survives an OBC switch (the ground sees an NVRESTORE flag); naming a file in [dhs] nvram_file persists it across simulator runs too. Nothing else in the platform ever touches the disk. The telecommand path never writes the file itself, see The one file-writing site.


10. The on-board database

The parameter file is read by one node. The DHS holds the mission’s parameter database on behalf of the whole spacecraft and distributes it over the bus; the other four subsystems never open the file.

Three layers

ROM (DP_CFG_CATALOG) → the modelled MRAM baseline (MOB1) → the operator’s non-volatile delta (MNV1), each applied over the last; obdb.md §5 is the model. The two persistent layers land in the same field, so one rule keeps them apart: a telecommand writes the delta, never the baseline — why prvReloadTunables is [dhs]-only and prvDhsReboot must not reprogram from the file.

The MRAM image

prvObdbPack/prvObdbLoad (:2006) model an MRAM image, deliberately the same shape as the parameter mirror — same header length, same 8-octet record, same CRC — so the difference between the two is semantic rather than structural.

0..3    magic "MOB1"
4..5    format version
6..7    record count
8..11   database epoch
12..13  CRC-16-CCITT over the whole image with these two octets zeroed
14..15  reserved
16..    records, 8 octets: id(2) type(1) width(1) value(4, right-aligned BE)

A record whose parameter has gone away or changed type is dropped on load — the catalog is the authority on what exists. An absent, short or CRC-failing image is non-fatal: the DHS programs the database from the parameter file instead, clamping every value into its declared range (a malformed INI value would otherwise distribute platform-wide rather than stay local).

Distribution

Each subsystem gets its own block, selected by the identifier’s high octet. An item carries only the low octet, so a typed parameter fits in a single 8-byte frame.

FramedlcLayout
CMD_CFG_BEGIN (0x26)7epoch(4), item count(1), flags(1)
CMD_CFG_ITEM (0x27)8sequence(1), pid low octet(1), type(1), value(4)
CMD_CFG_END (0x28)8epoch(4), count(1), CRC-16(2)

The checksum covers what the receiver will reconstruct, not the frames themselves, so a duplicate or reordered item still checksums alike. The receiver (common/obdb.c, in every node) stages the items, checks epoch, completeness and CRC, validates every parameter against its declared type and range, and only then applies all of them, or none — answering Ready, or a NAK: INCOMPLETE, CRC, EPOCH, PARAM, OVERFLOW.

Convergence

A block is re-sent until acknowledged at obdb_retry_ms; an acknowledged one is refreshed far more slowly at obdb_refresh_ms, as a backstop — one rule covers a late boot, a missed frame, and a cold reboot onto a redundant board alike. A subsystem that never answers is reported once as EVT_CFG_FAILED, then retried quietly.

prvObdbTouch bumps the epoch whenever a telecommand changes a distributed parameter, so a remote threshold retunes and re-distributes automatically; TC DHS CONFIG <node> forces it by hand.

The readiness mask (one bit per peer) rides in the DHS’s housekeeping. When it first reaches full, the DHS announces the platform mode once — otherwise it would hold its boot mode while every subsystem assumed NOMINAL (prvSetMode no-ops when unchanged), and convergence is the first point the subsystems are known to be listening.

Board straps stay with the box

A node still reads its own log_level, TTC its spacecraft identifier, and the EPS which unit of each redundant pair is active — facts about the hardware rather than mission parameters. See obdb.md §7.


10.5. The mass memory file partition and service 23

The file partition (§5) is also the RMAP target window for the SpaceWire mission data link — the only region of this node another card can write, bounds-checked on every access — and PUS service 23 is the link’s control plane. Both are documented in full on that page, including the two copy routes, the repository namespace, and why a file in the payload’s repository reports a length of zero.

Service 23 is DHS-local, like services 20 and 11, for the same reason: a file name alone is twelve octets, too big for the eight-octet CAN-command translation path.


11. On-board time

The DHS is the clock master. On boot no absolute time exists, so every node’s clock free-runs from zero on its own FreeRTOS tick and telemetry is flagged unsynchronized (the CUC “spacecraft time reference status” nibble in each PUS TM header, surfaced on the ground as +Ns (unsynced)).

The ground sets it with one telecommand — TC DHS TIME NOW, PUS 9 / subtype 128. The DHS latches its clock (obtSet), broadcasts it immediately, and raises EVT_CLOCK_SET. Thereafter prvBroadcastTime (:1639) re-broadcasts CMD_SET_TIME every time_sync_period_ms; the other four nodes discipline common/obt.c to it. The broadcast is suppressed while the DHS is itself unsynchronized.

Time is carried everywhere in CCSDS CUC form: 32-bit coarse seconds from the CCSDS epoch (1958-01-01) plus 16-bit fine sub-seconds, UTC. The epoch bridge to Unix time lives only at the ground edge (egse_tm rendering, egse_tc TIME setting); on board, time is just a count of seconds the DHS hands out.


12. Redundancy

All three cold-redundant pairs are owned by the EPS, which switches power; eps.md §10 is the model. What is local to this node: there is one DHS process, so “the active OBC” is EPS bookkeeping rather than a second binary, and subsystems only ever address the logical DHS (0x1).

prvDhsReboot

The DHS detects the switch in prvIngestHk when EPS_RPT_REDUN reports an OBC other than the one it believes hosts it, and models a cold boot of the backup (:609).

WipedSurvives
Mass memory, event log, TM sequence and message countersThe non-volatile parameter mirror and the on-board database — by definition not volatile
The pending verification table and the command scheduleThe schedule’s statistics, via that mirror (The safe-guard mirror)
In-flight file reassemblyThe [dhs] mission baseline, reloaded by prvReloadTunables, with dpRebindVolatile() re-applying stored values over it
The whole platform mirror and every ground-defined parameter
The clock (obtReset, back to unsynchronized)
Every peer’s configured state — this computer has no record of who acknowledged what

Read-only pool entries return to invalid, since their backing fields were just zeroed. The DHS always comes up in SAFE, forced onto the bus directly rather than through prvSetMode (a no-op if the mode already matches). Do not add auto-recovery that undoes this — only an autonomous low-power SAFE auto-recovers. The active ADCS and TTC boards reset to CPU_ID_UNKNOWN rather than zero, since asserting board A would make the first report from a spacecraft on B look like a switch that never happened.

ADCS and TTC CPU boards

On an ADCS- or TTC-CPU switch the DHS does not reboot. It tracks xPlat.adcs_cpu / xPlat.ttc_cpu, raises EVT_ACS_SWITCH / EVT_TTC_SWITCH, and marks that subsystem’s database block stale so the next pass reconfigures it. The platform clock, mass memory and mode carry on.


13. Concurrency and invariants

Lock hierarchy — the pool mutex is a leaf

LockGuardsTaken by
xSsmmMutexxTm + xSsmm + xEvlogevery path that builds a packet
xVerifMutexxVerifdispatch (track, match) and orchestrate (sweep)
xSchedMutexxScheddispatch (insert, delete, reset) and orchestrate (release)
xSsmmFileMutexucSsmmFile + xSsmmFilesthe SpaceWire receive task (RMAP writes) and dispatch (service 23)
the pool’s own mutexthe parameter table and arenadp* calls

None of the five ever nests. A read path snapshots into a dp_sample_t[], releases the pool lock, and only then builds a packet — holding the pool lock across prvEmitTm/prvRaiseEvent (which take xSsmmMutex) would create the one cycle that matters. Verification reports are likewise emitted after xVerifMutex is released, and the schedule copies its due entries out under xSchedMutex, releases it, then reports — why prvSchedReport snapshots into fixed-width records rather than copies of the (96-octet) entries.

One mutex covers the three telemetry structures because every path that builds a packet touches all of them, keeping the sequence counters race-free across the five emitting tasks. xSsmmFileMutex joins on the same terms and is the easiest to get wrong: the SpaceWire receive task holds it while writing into the mass memory window and service 23 holds it while reading a file out, so every service-23 path snapshots the file record (and, for a downlink, its octets) under it, releases, and only then calls prvEmitTm.

Posix port hazards

The DHS is flight software and carries all of them — see porting.md. Its heap budget is seven tasks (five in main(), canrx, spwrx) — why the SSMM, event log, pending table, mass memory file partition and data-pool tables are all static, and why the pool’s “dynamic” block is a statically sized arena with an allocation bitmap rather than pvPortMalloc.

The one file-writing site

The NVRAM mirror and the MRAM image are the only files a node writes. dpNvramSync on the telecommand path only marks the mirror dirty; both files are written from prvOrchestrateTask on a cadence divider, never from the dispatch path, and reading them is safe only in main() before the scheduler starts — getting this wrong starves the port, it doesn’t merely slow it.


14. Configuration reference

The [dhs] section of config/minscs.conf. Every key has a compiled-in default, so a missing key — or the whole file — just leaves the built-in value in place and the platform still boots. Read by prvReloadTunables (:4754), except log_level, which prvLoadConfig (:4855) applies before the first banner — prvReloadTunables runs again on a reboot and is [dhs]-only by design (§10).

KeyDefaultPool idMeaning
log_levelinfoofftrace
default_modeNOMINALBoot mode. The shipped file sets SAFE
hk_poll_period_ms5000Fallback HK poll cadence and quantum ceiling
orchestrate_tick_ms1000ORCH_TICK (RO)Decision-loop period; divisor of every cadence below
time_sync_period_ms5000TIME_SYNC_PERClock re-broadcast cadence
hk_tm_period_ms5000HK_TM_PERIODThe DHS’s own HK cadence
verif_timeout_ms3000VERIF_TIMEOUTWait for a completion ack before TM[1,8]
soc_resume_pct50.0SOC_RESUME_PCTSoC above which the DHS leaves an autonomous SAFE
therm_limit_hi_c45.0THERM_LIMIT_HIBattery over-temperature alarm
sched_enabled1SCHED_ENABLEDBoot state of the command schedule; TC[11,1]/[11,2] move it
sched_past_margin_s2Refuse an insert whose release time is already this far behind the clock
fdir_period_ms2000How often the FDIR task samples its monitors
fdir_enabled1FDIR_ENABLEDMaster switch for both monitors
fdir_adcs_ticks5FDIR_ADCS_TICKSUnchanged ADCS heartbeats before COMM_TIMEOUT
fdir_bus_uv_v26.5FDIR_BUS_UV_VBus undervoltage limit, volts
fdir_uv_ticks3FDIR_UV_TICKSConsecutive ticks under the limit before safing
nvram_file(empty)Parameter mirror backing file; empty = memory only
nvram_flush_period_ms10000How often the decision loop may write it
obdb_file(empty)MRAM image backing file; empty = reprogram from the file each boot
obdb_retry_ms5000Re-send an unacknowledged block
obdb_refresh_ms60000Re-send to a ready subsystem, as a backstop
hk_<sub>_enabled1HK_*_ONCollect this subsystem: poll it and downlink it
hk_<sub>_period_mshk_poll_period_msHK_*_PERPoll cadence for that subsystem

<sub> is adcs, eps, ttc or payload (the key names come from canAddrName).

nvram_file and obdb_file take a bare filename, resolved against the config file’s directory by prvResolveCfgPath (:4729) — CFG_VALUE_LEN silently truncates a long value, so a full absolute path would be cut off on the way in.

Structural constants that size static arrays — SSMM_SLOTS, EVLOG_SLOTS, VERIF_PENDING_SLOTS, SCHED_SLOTS, SCHED_TC_BYTES, REASM_MAX_BYTES, OBDB_MAX_ROWS — stay compile-time, because the heap budget is computed against them.


15. Known gaps and deliberate simplifications

  • A payload file with sequence gaps is discarded, not re-requested (prvHandleConsecutiveFrame logs "would re-request on a real mission") — the ISO-TP-style transport has no flow control by design.
  • A file larger than REASM_MAX_BYTES (8192) is refused at the First Frame (:1156), and a new First Frame abandons an in-progress reassembly — one reassembly context, not one per source.
  • EVT_CFG_PROGRAM (0x0014) is declared but never raised — programming the database from the file logs but does not emit an event.
  • prvSetMode no-ops when the mode is unchanged, so a mode telecommand requesting the current mode reports TM[1,7] without putting anything on the bus; the convergence-time announcement covers the boot case.
  • The DHS’s own housekeeping carries no other subsystem’s measurements, by design — every octet under APID 1 is a parameter this computer owns, at the cost that no single packet is a whole-spacecraft summary.
  • The Posix port is explicitly not real-time. Scheduling is illustrative; the on-board clock is disciplined to whatever UTC the ground sets, not to a real-time guarantee.
  • ADCS_HB is an octet, so a frozen counter is not the only way to look dead. At the shipped 10 Hz rate consecutive samples are never equal, but a legal TC[20,3] retuning ADCS_HK_PER can put an exact multiple of the control period between reports, aliasing a healthy ADCS into a frozen count. A wider counter pushes the aliasing out rather than removing it; the honest fix is a monotone timestamp, more than this proof of concept needs.
  • FDIR recovery actions are compiled in, fixed in prvFdirTask rather than data the ground can rebind, and neither monitor is a PUS service 12 definition — service-12 shaped (parameter, threshold, persistence count, transition report) but the service is absent. Service 19 (event-action) is likewise absent. Also missing and FDIR-relevant: 5,5–5,8 (inhibiting a chattering monitor’s event reports) and service 15 (anomaly forensics — the SSMM ring is deliberately not modelled as it, and the event log is dumped by a service-8 opcode instead).
Last updated on