Skip to content

The data pool and the on-board database

Two related mechanisms, often confused. The data pool is one addressable, typed, access-controlled view over on-board state, per node. The on-board database is the mission’s parameter set, held by the DHS and distributed over the bus to the other four subsystems.

Source: include/datapool.h (the catalogs DP_PID_CATALOG and DP_CFG_CATALOG plus the pool API — FreeRTOS-free, so the ground tools share one set of mnemonics with the flight code), common/datapool.c (table, arena, NVRAM mirror), and common/obdb.c (the receiver, linked into every node). Wire formats and frame layouts are in dhs.md §9 and §10; this page is the model.


1. The pool binds, it does not copy

The pool stores descriptions, not values. Each entry binds a pointer to the C field that already holds the datum — the platform mirror the DHS fills from housekeeping (&xPlat.soc_pct), the tunables it loaded from minscs.conf (&xSys.verif_timeout_ms), the housekeeping-collection table (&xHkTable[i].enabled), its own workload counters.

There is therefore exactly one copy of every value on board and no mirror to keep in step. What matters operationally: the parameter file is the pool’s baseline, not its only word — the ground retunes a threshold in flight and the flight code reads the new value wherever it already read the old one.

Adding a parameter is one DP_PID_CATALOG row and one dpRegisterCfg/dpRegister call in the owning node. Do not add a config key per parameter; that is what the binding exists to avoid.

2. Identifiers

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

Within a subsystem’s space the low octet splits the two kinds of parameter:

Low octet below 0x80 is a measurement; at or above it is on-board database content (DP_PID_IS_CFG).

The threshold was 0x10 until the ADCS interface needed sixty-odd identifiers; it moved for every subsystem rather than the ADCS alone, because a rule an operator can apply to any identifier on sight is worth more than a per-subsystem exception. Moving it again invalidates a stored MRAM image, which is why OBDB_VERSION exists — a stale image is rejected and reprogrammed rather than applied to the wrong parameters. Bumped four times:

VersionWhat moved
2The mirror band was widened, moving every database identifier
3Eight EPS parameters left the database for [eps_dyn] — the orbit’s shape, the cell’s true capacity and the thermal constants describe the spacecraft, not a decision the flight software makes. Four gauge-calibration parameters replaced them (eps.md §11).
4The payload’s instrument rail became three switched channels, retiring instr_safe_a / _nominal_a / _science_a (0x04890x048B) for six parameters at 0x04980x049D
5The payload gained camera_image_bytes and spw_rmap_bytes (0x05820x0583) with the SpaceWire link. Nothing was removed, but an older image would be applied as a complete payload block, leaving both at their ROM defaults for good with nothing to say so.

At 3 and again at 4, replacements took new identifiers rather than the vacated slots: the non-volatile delta has its own version gate, independent of this one, so a reused identifier could land an operator’s stored override on a parameter that means something else. Holes in a 128-slot band cost nothing; a silently misapplied override costs a debugging session.

3. A pool is per-node, and that is a feature

Each process has its own. The ADCS’s local pool holds all 65 identifiers of its sensor/actuator interface at 10 Hz and is not reachable from the ground; the DHS’s central pool holds only the 24-identifier summary the ADCS echoes over CAN once a second. The same mnemonic can name an entry in both — unambiguous precisely because a pool never leaves its node. Corollary: do not add a central-pool parameter for a sensor the DHS has no wire to.

4. Ground-defined and derived parameters

A ground telecommand can define a new parameter in a dynamic block — free-standing, or derived from one or two existing parameters through a typed operator — and delete it again. The pool also models a non-volatile mirror so an operator’s retuning survives a reboot the way real stored context does.

A derived parameter is a typed descriptor over identifiers, never a byte range. ECSS describes a parameter “mapped to combined offsets of existing parameters”, and the tempting reading — let the ground uplink an offset and a length into on-board memory — is the wrong one: it is unverifiable on board, it breaks silently the moment a struct field moves, and it turns a telecommand into an arbitrary memory read. Naming identifiers and an operator keeps the ground inside the same access-controlled, range-checked, typed table it reaches through every other subtype. This is also why PUS service 6 (memory management) isn’t implemented on this platform, and why the ADCS interface guard borrows only [6,9]/[6,10]’s checksum idea rather than the service itself — see The interface guard.

Derivation depth is exactly one. dpDefine refuses a source that is itself derived, so evaluation is O(1) and cycle-free by construction, with no visited set and no recursion on a flight stack.

The dynamic block is a statically sized arena with an allocation bitmap, not a heap: there is no pvPortMalloc in the pool, for the heap-budget reason.

Access rights and persistence are independent. dpNvramSync/dpNvramRestore gate on DP_ACC_NVRAM alone, never on DP_ACC_WRITE, so the schedule statistics (STAT_SCHED_REL 0x0148, STAT_SCHED_FAIL 0x0149, STAT_SCHED_DROP 0x014A) are read-only and persistent — the right shape for a counter measuring what the spacecraft did, which a TC[20,3] must not forge. Their sibling SCHED_COUNT (0x0147) is read-only without NVRAM, because the queue it counts is wiped by a reboot and a restored depth would report a schedule that no longer exists. See The safe-guard mirror.

SYS_SW_VERSION (0x010A) is neither measured nor stored — the compiled-in build identity from include/version.h, the one entry prvMarkOwnFactsValid promotes rather than leaving DP_ST_UNDEFINED, at boot and again after a reboot (which models a different computer running the same build). It changes no stored image: DP_PID_IS_CFG requires a high octet of 0x020x05, so nothing in the DHS’s own band can be database content. Why it’s also on the downlink is in Which simulator this database describes.

The operators, the query-versus-mutation rule and the mirror’s boot order are in Parameter management.


5. The on-board database: three layers

The parameter file is read by one node; the other four never open it — five processes sharing a filesystem is not how boxes on a bus learn what they’re supposed to do. Three layers compose, always in this order:

LayerWhere it livesWhat it is
ROMcompiled into each node (DP_CFG_CATALOG)what a box runs on before it hears from the database, and keeps running on if it never does
OBDBthe DHS’s modelled MRAM (MOB1)the mission baseline as programmed, from config/minscs.conf
operatorthe non-volatile mirror (MNV1)the delta a telecommand has commanded in flight

The two persistent layers have deliberately different meanings and deliberately identical structure: a sixteen-octet header of magic, format version, record count, one 32-bit word (the database epoch for MOB1, a wear counter for MNV1) and a CRC-16-CCITT over the whole image with its own two octets zeroed, then eight-octet records. Shared enough to share the code (dpImageHeaderPut / dpImageHeaderCheck in common/datapool.c), while each keeps its own record body. dpImageHeaderCheck separates absent or stale from corrupt, because only the second is worth warning an operator about: a version mismatch is a deliberate re-program (see OBDB_VERSION), a failed CRC means the bytes rotted.

Both lower layers are persistent and both land in the same field, so one rule keeps them apart:

A telecommand writes the delta, never the baseline.

The database is reprogrammed only by a first boot or an explicit ground command, so what is the baseline? always has an answer. prvDhsReboot must therefore not reprogram the database from the file; prvReloadTunables is [dhs]-only for exactly that reason.

6. Boot in three steps

The whole of it is visible in the telemetry stream as events:

  1. The DHS configures itself. It builds the database from its MRAM image — programming it from the parameter file if there is no valid image — and binds every row into its own data pool, reading [dhs] for itself.
  2. The DHS distributes. Each subsystem gets its own block: an open (CMD_CFG_BEGIN) carrying the generation and a count, one frame per parameter (CMD_CFG_ITEM), and a close (CMD_CFG_END) carrying a checksum. A parameter’s identifier names its owner in the high octet, so “the ADCS’s block” needs no separate table, and an item need only carry the low octet — what makes a typed parameter fit in a single eight-byte frame.
  3. The subsystem acknowledges. It stages the items, checks the block arrived whole and intact, validates every parameter against the type and range the database gave it, and applies them — all or none. Then it answers Ready, and the DHS’s housekeeping shows one more subsystem holding the current generation.

A block is re-sent until acknowledged, the same idea the EPS uses to announce the active CPU board: an owner re-announcing authoritative state to an idempotent receiver. That one rule covers a node that boots late, one that missed a frame, and one that cold-rebooted onto its redundant board.

Until its block arrives a subsystem runs on its ROM defaults and says so, appending (rom) to its status line exactly as an undisciplined clock reads (free). It is not held inert: a box that refused to work until configured would have no way back if the DHS never came up.

7. Board straps stay with the box

A handful of keys stay in the file because they are facts about the hardware rather than mission parameters. The list and the reason for each are in config.md §2. The redundancy state is the load-bearing one: the high-priority switch telecommand reaches the EPS without the DHS in the loop precisely so it survives a DHS that’s down, and a DHS-distributed block would undo that.

Everything else stays reachable through service 20: a subsystem’s parameters are in the DHS’s pool, so TC DHS PARAM SET EPS_SOC_LOW F32 20 retunes a threshold three hops away, bumps the database generation, and re-distributes automatically. TC DHS CONFIG <node> forces a re-distribution by hand. The frame layouts, the NAK codes and the retry/refresh cadences are in dhs.md §10.


8. Concurrency: the pool mutex is a leaf lock

The pool’s mutex guards the parameter table — definitions, status flags, update times and the dynamic arena — and it’s a leaf: never held across telemetry generation, event raising, canSend, or file I/O. A caller that needs several values snapshots them into its own dp_sample_t array, releases the lock, and only then builds a packet.

That ordering is what makes deadlock structurally impossible rather than merely unlikely. The DHS holds three other locks — xSsmmMutex over xTm+xSsmm+xEvlog, xVerifMutex over xVerif, and xSsmmFileMutex over the mass-memory file partition — and telemetry generation itself takes xSsmmMutex. Holding the pool lock across prvEmitTm or prvRaiseEvent would close the one cycle that matters. None of the four ever nests inside another; see Concurrency and invariants.

A known limitation, inherited rather than introduced. Some bound fields — the DHS platform mirror, the telemetry counters — are already written lock-free by more than one task. The pool mutex doesn’t guard those fields, only the table describing them. They’re naturally aligned scalars of at most four octets, read and written exactly as the surrounding code already reads and writes them, so binding a pool entry to one neither adds a race nor removes the one that was already there. The FDIR state (xFdir) is the deliberate version of the same design: every word has exactly one writer, which is why it needs no mutex at all.

Last updated on