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
0x80is 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:
| Version | What moved |
|---|---|
| 2 | The mirror band was widened, moving every database identifier |
| 3 | Eight 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). |
| 4 | The payload’s instrument rail became three switched channels, retiring instr_safe_a / _nominal_a / _science_a (0x0489–0x048B) for six parameters at 0x0498–0x049D |
| 5 | The payload gained camera_image_bytes and spw_rmap_bytes (0x0582–0x0583) 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
0x02–0x05, 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:
| Layer | Where it lives | What it is |
|---|---|---|
| ROM | compiled 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 |
| OBDB | the DHS’s modelled MRAM (MOB1) | the mission baseline as programmed, from config/minscs.conf |
| operator | the 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:
- 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. - 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. - 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.