ECS for Control: Concepts
The Entity Component System architecture for real-time control programs: the store, the invariants that hold it, partitions and their seams, and where every definition lives.
Motivation
A control system meant to run unattended has to do something when a fault strikes it at run-time. The simplest response is to restart the whole program – reset, re-initialise, begin again. For many systems that is not acceptable: a flight controller, a spacecraft, a process line cannot drop everything and reboot each time a sensor glitches or a computation goes wrong. They have to keep running – to recover the affected part and carry on.
Any recovery short of a full reset needs one thing before all others: a valid picture of the control system's current state to recover from. To restart a controller, resume a sequence, or drop to a safe mode, you have to know where the control system actually is – and that picture must exist, coherent and readable, at the moment the fault is handled. Where that state lives, and how readily it can be captured, is fixed by the architecture of the program, long before any recovery code is written.
Most architectures can be made to provide it – typically by having each part of the program periodically write its recoverable state out to a separate record. But that is extra machinery, maintained by discipline and easy to get wrong. There is one architecture in which a coherent picture of the whole control system's state is not something you add on, but the central organising idea – where the state you would recover from is simply the control system's ordinary representation. That architecture is the Entity Component System (ECS): all state kept in one store, operated on by procedures that hold no state of their own.
ECS does not come from control engineering. It comes from game engines, and the difference in origin marks exactly what carries over and what stays behind. A game runs an open world: entities spawn and die at any moment, and what exists – which entities currently carry which Components – must be discovered, at run-time, over and over; a large share of a game engine's machinery exists to make that discovery fast.
A control program runs a closed world: the controlled system is known when the program is written – every entity, every Component instance, every System enumerable at design time. Membership is authored, not discovered: what exists is written down, and what is written down can be verified once, before the first tick, and trusted thereafter. What control takes from ECS is therefore the structure – all state in one store, stateless Systems over it, declared access – and none of the discovery machinery, which answers a question a control program never has to ask.
Set apart from its origins, that structure lands squarely inside established real-time control practice and the standards that formalise it – the cyclic executive, synchronous dataflow, partitioned architectures – as we come to below. And while recovery selected the architecture, it is not what carries it alone: a state that is coherent and visible at every tick, dependencies that are declared rather than hidden, a schedule that is derived rather than written – each stands on its own, and the chapters below develop them in their own right. Recovery remains the horizon this evaluation works toward.
Detection is recovery's other half: a fault must be seen before anything can be recovered from it. The per-event path – run-time checks, faults – is one feeder; the other is watching conditions no single event can show, over time. That second feeder is the subject of the Observations set; this document builds the ground both stand on.
Evaluation Programs
The concepts described here are exercised by three generations of evaluation programs: the EcsControlBase variants of the first round, EcsControlStore of the second, and EcsControlWatch, the third round's program – partitioned, with the observation machinery live and verified on target. The implementation document describes the mechanisms as EcsControlWatch implements them; a walk-through of the program itself is to follow as its own document.
The Concepts
ECS names its three elements after the things it keeps apart:
-
Component – data, and only data: a record belonging to something in the control system (a motor's measured angle, a controller's gains, an LED's on/off state). No behaviour.
-
System – a procedure, run once per tick, that reads some Components, computes, and writes results back to Components. It holds no state of its own between ticks; everything it needs is in Components.
-
Entity – an identity, and only that: a bare index that groups the Components describing one thing (one motor, one joint, one LED). No data, no behaviour.
Two qualifiers keep this document precise, because "Component" is used in two senses. A Component type is a declared form of data – the PID Component, the blink configuration. A Component instance is one allocated record of such a type, holding values – this joint's PID state. A System is attributed the Component instances it reads and writes and, each tick, runs over them; an Entity groups one instance of each of the types that describe it. Entities need no such qualifiers: an Entity is only ever an index, and the collective – the joints, the followers – is a population, a bounded index range authored at design time, not a type. Where the distinction carries weight below, the qualifier is spelled out; an unqualified "Component" means an instance – a record holding values in the store.
The store is all the Component instances and their current values. Once a tick's Systems have all run to completion, it is the complete and sufficient description of the control system's state – in a two-fold sense made precise in Control System and Controlled System, below: the control program's state, the store is; the hardware's and the controlled system's, it represents.
Everything rests on that one property: all state is in Components; a System keeps none.
Stated plainly, three things follow:
-
The whole state is in one place, and coherent. Each tick runs the Systems once, in a fixed order, to completion; the store then holds a complete and consistent state – every Component carrying this cycle's value, none part-way through an update – and it stands until the next tick's Systems begin the following cycle. Obtaining a valid picture of the control system therefore needs no Systems and no coordination: you simply read the store.
-
The data dependencies and access roles are declared and machine-readable. Every Component instance has exactly one writer. The invariant attaches to the instance, not the type: instances of one type may belong to different writers – two Systems each producing the set-points of the entities they steer – but no instance ever has two. A writer is the producer of the instances it writes; the Systems reading them are their consumers. What a System writes and what it reads – its declared footprint – is the unit of dataflow, and from the declared footprints the order the Systems must run in is derived, a circular dependency detected, and a second writer rejected – all before the first tick. The data that form the control system's state are explicitly managed in one place, both at construction and at run-time.
-
The structure is fixed; only values change. The fixity has three levels, each settled one phase earlier than the next: the Component types are fixed at compile time, in the declarations; the instances – every Entity, every Component instance, every System – are created once, at creation, and none is added or removed at run-time; and tick to tick only the instances' values change, never the shape of the store. That is what keeps the model within hard-real-time, static-memory limits.
A Component is data, and its form is open: a Component type can be built from any data structure the language offers – a plain record with exposed fields, or an abstract data type with access procedures, such as a ring buffer. ECS constrains how the control logic is divided into Systems over the store; it says nothing about how a Component's data is structured.
What This Means for a Control Program
Take a jointed robot – limbs and joints – under closed-loop control, each joint running, say, a PID law. The usual way to organise this is to build a data structure that mirrors the machine: a tree of limbs and joints, each node holding the data for its own control, including its PID's running state. Each tick, the control loop walks that structure and runs the algorithm on every node. The code follows the shape of the machine.
ECS cuts it the other way. The per-joint control data is lifted out of the machine structure into a flat collection – one instance of the PID Component type per joint, side by side – and one PID System runs the law over all the instances each tick, knowing nothing of limbs, joints, or how the machine is assembled. It does one thing, across everything that carries an instance of its type. That is the whole shift: the code no longer follows the data structure; a System runs over the data.
The machine's structure has not disappeared – it has moved from the shape of the code to data in the store. A joint's place in the linkage is itself a Component: a reference to its parent, or to the chain it belongs to. The per-joint PID System never reads it, because servoing a joint to its set-point does not need the linkage; the System that does – one computing the joint set-points from a target and the geometry – reads those relationship Components and writes the set-points the PID System then follows. Structure is consulted where the problem needs it, not built into how every operation is dispatched.
The two arrangements cut the problem along different axes. The traversal organises the program by machine part – one piece of code owns a joint and everything about it. ECS organises it by operation – one System owns the PID law and applies it to every joint. Add a second concern, say a thermal limit per joint, and the difference shows: in the traversal each node gains a field and the walk does more work; in ECS it is simply another System over a new thermal Component type, its instances cutting across the same joints independently. Concerns that cut across the machine become Systems that cut across the store.
A control loop is a feedback loop; in ECS it runs through the store. A sensing System reads each joint's angle from its encoder and writes it as a Component; the PID System reads that angle and the set-point and writes a command Component; an actuation System reads the command and drives the motor. The Systems never call one another – each only reads and writes the store – and the loop itself closes outside the software, through the machine: the motor moves, and next tick the encoder measures where it moved to. Sensing deposits into the store, control transforms values within it, actuation withdraws them; the data has a defined place, and the Systems visit it in turn.
None of this is exotic to real-time control; it is largely familiar. Stateless operations, run in a fixed order to a periodic tick over a shared body of state: that is the shape of the cyclic executive that IEC 61131 programmers already write, of the synchronous-dataflow languages (Lustre, SCADE) behind certified flight software, and of the time-partitioned architectures of avionics (ARINC 653). ECS is a data-oriented member of that family. What it adds is the discipline that all state – not only the shared signals – lives in the one declared store.
If organising the control logic as Systems over a shared store – each communicating through Components – looks like overhead compared with the traversal that just walks the machine's structure, weigh it against what that walk costs. It moves from node to node and, at each one, works out what kind of node it is and runs the operation for that type: heterogeneous work, data-dependent branching, memory accesses that hop from node to node.
A System instead runs down one array – the instances of a single Component type – and does exactly the same operation on each: no per-node decision, no branch that turns on the data.
At the scale a control system works – a handful to a couple of dozen Components of a kind, not the thousands of a game – the absolute cost is not decisive either way, so the point is not speed but shape: a System's cost is a fixed count of identical operations, the same every tick, and a worst-case timing analysis can take it almost at a glance.
Dataflow Roles
A System's declared access has two sides – what it produces, what it consumes – and the traffic through the store falls into two classes.
Produced and consumed is the ordinary class, and nearly everything is in it: one System writes an instance, other Systems read it, and each such edge orders the producer ahead of its consumers. The sensing → control → actuation chain of the previous chapter is this class end to end.
One consumption mode completes the ordinary class: a consumer may declare that it reads the previous tick's value of an instance. The edge then orders the other way – the consumer may run before the producer, reading what the last cycle left – and the mode has two uses, both deliberate: it breaks a same-tick cycle where two Systems legitimately feed each other, and it introduces a deterministic unit delay where the design wants one – a command taking effect one cycle after it is issued, predictably, every time. The delay is declared, so it is part of the derived order's inputs like everything else.
Some concerns are not a chain but a gathering. Several Systems each contribute a piece of the same kind, and the contributions are read together: several Systems print a line of a report, one System drives the collected text out; several Systems record their progress, a monitor reads it across. Pressed into the produced/consumed mould, every contributor would need a private Component and a private edge to the gatherer – a declaration that grows with every participant, for what is conceptually one facility. The architecture names the shape instead:
Deposited and collected is the class for shared facilities. The class contract does not change with the facility's shape: every participant deposits, and its participation is declared; a collector reads the gathered whole, ordered after every depositor – at least one must exist, any number may, and every one of them is so ordered. Both sides declare the role: a facility is deposited-and-collected or its Components are produced-and-consumed, never both, and reading one class through the other's role is rejected at construction like any structural violation.
The facility's storage comes in two kinds, chosen by whether attribution matters:
-
the indexed kind: one slot per participating System, each participant depositing into its own – the single-writer invariant over again, per slot – and the collector reads every slot. Contributions stay attributed: whose progress word, whose measurement;
-
the stream kind: one shared transport – a queue, a ring – in which every participant's deposits interleave, and no identity is attached to the payload. Deliberately multi-producer, and sound under the schedule's own guarantees: no two Systems ever run at once, and a System runs to completion, so every deposit is whole before the next begins; the transport's protocol covers the rest. The fit where the facility is an aggregate and its load is an aggregate fact – a shared print stream, whose overload no participant owns.
An indexed facility's instances are indexed by the participating Systems, not by the controlled entities – a service space. Each participant owns its slot under an identity of its own, and the identity is per participation: a System depositing into two facilities holds two unrelated identities, and no facility needs – or has – a global enumeration of Systems behind it. Adding a participant costs what the kind implies – a slot and its wiring in the indexed kind, one declaration in the stream kind – and the collectors change in neither, because they read whatever the facility holds.
Service spaces also sharpen where the control domain ends. What belongs to the control program's picture is defined by what its Systems touch, not by what exists. The scheduler's own counters – ticks elapsed, ticks missed – are platform state like any timer register, until a System samples them and deposits a report; from that moment they are measurements in the store, as visible and recoverable as a sensed temperature. Call it enrollment: a quantity becomes control data when a System senses or commands it, and by no other means. Control System and Controlled System, next, generalises the criterion.
Cross-cutting concerns – reporting, progress, the counting of observed events, monitoring – thus cost the architecture nothing new: a facility is Components, its participants and its collector are Systems, and the declared classes carry the whole arrangement.
Control System and Controlled System
Before descending into layers: the terms of engagement – what is what. There are two systems, and every element belongs to one of them, or it is irrelevant.
The controlled system is defined by the control laws: its boundary is exactly the set of quantities the laws sense and actuate. That makes the definition testable, in both directions.
A law that depends on a quantity with no sensing path is a definition error – either the room temperature is measurable, or it is not part of the controlled system; if the laws need it, the definition is incomplete. A sensor that is defined but no longer used is the same error from the other side – surplus, and it accrues silently as the control system evolves. The definition is exact when what is sensed and actuated equals what the laws need; and because consumption is declared, the surplus side is checkable: a measurement Component with no consumer is provably dead, and its sensing System, its binding, and its configuration follow it out.
The control system is everything the control laws require in order to run: the code that computes them, the hardware through which they sense and actuate, and the platform that makes their execution valid. Three clauses, three regions:
-
the control program – the store, the Systems, the bridging units, the HAL code, and the run-once creation code;
-
the interface hardware – the control-path peripherals: the GPIO block, the UART with its FIFO, the timers in use, a sensor chip on its bus;
-
the infrastructure – clock tree, power, memory: it carries no loop data and feeds no law. The laws do not use it; they presuppose it.
The boundary between the two systems is drawn by authority: inside the control system lies what creation can configure and lock; outside lies what steady state can only command and measure. Authority and wiring are two sides of one design decision: the intended authority determines the wiring – which devices sit on which bus, what is soldered where – and the wiring, once built, bounds the authority on offer; with a given board, what can be configured and locked is no longer a free choice. The wiring is thus the earliest level of the fixity ladder – wiring at board design, types at compile time, instances at creation, values in steady state – each level binding all later ones.
The criterion reads the boundary off the authority actually available. At the plant edge it usually coincides with the pins – the GPIO peripheral is control system; the LED behind the pin, unconfigurable, only ever lit, is controlled system. But the pins are the common case, not the criterion: a sensor chip reached over a bus lies beyond them and is inside all the same – configured and locked like any peripheral. Its readings are another matter, and that distinction carries the whole scheme: the machinery of sensing belongs to the interface hardware; the quantity being measured belongs to the controlled system; the values, as measured, belong to the store.
Within the interface hardware, the same criterion recurs one level down. Its structure – pin functions, which devices exist and are wired – is configured and locked at creation. Its configuration values – a baud rate, a threshold – may change in steady state, within the fixed structure, and only through the store: produced by a System, like any other commanded value. And the regions are fixed by role, not by part: a program that must monitor its clock gives that measurement a Component and a sensing path like any plant quantity – while the clock as platform remains infrastructure. This is the enrollment of Dataflow Roles, stated as the general rule.
One agent does all the configuring: the control program, acting in two modes – as creator, run-once sequential code from reset, building everything below and around itself; and as controller, the steady-state machinery of Systems on the tick. World Creation and Steady State, below, gives the creator its sub-phases.
What, then, does the store hold? Two claims, about two different things:
-
the store is the control program's state – containment: "all state is in Components" quantifies over the control program, and over nothing else;
-
the store represents the interface hardware and the controlled system – commanded values and configuration outward, measurements inward, each as commanded and as measured.
The consequences run through the rest of this document. The hardware's own state – a FIFO's content, a latch – is never in the store, and need not be for the store to be the complete picture of what it claims to contain. Measured values are the sample and its age, never the plant's actual state, which can only ever be re-learned. And representation demands agreement: established at creation, maintained in steady state by actuating and sensing, re-established by recovery.
Representing devices takes machinery of its own – device records, buffers, the working apparatus of drivers – and that machinery is program state of a third kind, belonging to neither claim. The control program's state therefore splits in two. Control state is the store: values, and handles – stable names, not addresses, by which devices and service machinery are referred to. Technical state is the apparatus behind those names, and it lives with the modules that operate the devices – distributed, not gathered in any central place.
The split is a lifecycle split: control state is what a snapshot captures and recovery re-values; technical state is re-established in place, rebuilt beneath unchanged handles – a handle in the store survives a recovery, what it resolves to is the technical side's business. And the split is what keeps the store copyable at all: no address, no pointer into machinery ever sits in a Component; the store is values and names, and both move whole.
Layering
Even the simplest control program has to reach hardware, and where it does, a program built this way falls into three layers:
control logic control laws, synchronous, on the tick
-----------------------------------------------------------
bridging layer synchronous / asynchronous / hybrid
-----------------------------------------------------------
HAL thin: locate and touch hardware
-----------------------------------------------------------
hardware
Control Logic
At the top is the control logic – the Systems carrying the control laws. They run on the tick and communicate only through the store, never calling one another and reaching hardware only through the layers below. Everything the earlier sections describe – the derived schedule, tick-boundary consistency, all state in Components – is a property of this layer.
Bridging Layer
Between the control logic and the hardware sits the bridging layer (formerly: the adaptation layer), present wherever a hardware interaction cannot be done in a single step.
When it is needed. Some hardware interactions are immediate: a GPIO write, reading a value register – one access, done at once. For these a control System reaches the HAL directly; there is nothing to bridge and no layer in between. Others are not immediate – they unfold over time or over several steps, and so carry progress state, a record of how far along the interaction is.
Streaming a line of text to a UART is one: the bytes leave one at a time, far slower than the control loop can wait for. An I2C transaction is another: start, address, acknowledge, data, stop – a sequence, some steps waiting on the device. Anything with progress state needs somewhere to hold that progress and something to drive it forward – and simply waiting for the hardware is not an option: a System never busy-waits (Data Integrity and Consistency states why). Carrying the interaction across ticks instead is the bridging layer's work. So an LED, set by a single pin write, needs no bridging; the terminal, fed a report the UART emits over many ticks, does.
Three ways to run it. The bridging layer can be driven three ways, in either direction:
-
synchronous – an ordinary System, run on the tick, advancing the interaction a step at a time (send a byte; or check whether the transaction's next step is ready, and if so take it);
-
asynchronous – an interrupt handler, run at hardware time, advancing it as the hardware becomes ready;
-
hybrid – a System that sets the interaction up and an interrupt handler that carries the rest.
A control System therefore reaches a peripheral by one of a few paths:
-
straight to the HAL, for an immediate interaction;
-
by producing into a Component that a synchronous bridging System consumes and drives out;
-
by handing off to an asynchronous handler;
-
by handing off to a hybrid of the two.
Input mirrors output – a sensor read is immediate and direct, while a stream or a protocol arriving in passes back through the same bridging layer.
What surfaces as a Component. Which of an interaction's state appears in the store depends on how it is bridged. A synchronous bridging unit is a System, so by the one rule it keeps its progress in Components – its state sits in the store, as visible and recoverable as any control state. An asynchronous unit is not a System; its progress lives inside the layer, not in the store. (Why the asynchronous case must be that way is the subject of Data Integrity and Consistency, below; here it is enough that the choice of bridging settles it.) So the Components of a control program fall into three kinds:
-
control-domain Components – the control-law and measurement state the control task is actually about, together with everything that describes the task: the structure of the controlled system (a joint's place in its linkage), the sources a System draws its data from, parameters and configuration;
-
bridging Components – the working state a synchronous bridging unit needs;
-
bindings – the fixed hardware map the HAL is reached through.
The first kind is the widest: the other two are narrow by definition, and whatever is neither the crossing's working state nor a hardware map is control-domain. Reading which is which tells you, at a glance, how much of a program's dealings with the interface hardware and the controlled system are visible in the store and how much are hidden in the layer below.
The first round's evaluation programs walk this space on purpose: one control program, its report to the terminal driven synchronously, asynchronously, and as a hybrid in turn – so each bridging mode, and what it does to the Components, can be read off a working example.
Hardware Abstraction Layer
At the bottom is the hardware abstraction layer (HAL): the thin code that locates a peripheral and touches it – write a pin, read a value register. At its thinnest each access is a single, immediate step, and composing such steps into an interaction that unfolds over time is the bridging layer's work above it – the picture the next paragraphs start from, and then qualify.
Where the HAL lies on the control path – the path by which the control logic senses and actuates – and is driven synchronously, two conditions keep it thin:
-
it is stateless – its code holds no mutable run-time state, only a device record fixing what locates the hardware (a base address, a pin, a fixed configuration), set once at creation and never changed after; anything an interaction must remember lives in Components, not here;
-
it is passive – it is called and never calls back, starting no activity of its own; it acts only when the control logic or the bridging layer calls it.
The first keeps every piece of recoverable state in the store; the second keeps all behaviour and timing in the control logic and the bridging layer, where the program controls them. Between them they ensure the HAL adds neither hidden state nor hidden control flow beneath the store.
Stateless is a condition on the HAL's code, not on the silicon. Hardware always holds state – an output latch, a shift register, a transmit FIFO; even the single pin write lands in a register that holds the value written.
That state is not the HAL's: it is the interface hardware's own, and on the synchronous path the program deals with it as it deals with the controlled system's – sensed and commanded on the tick. A FIFO's full flag is something a System reads, fresh, each time it needs it; writing a byte into the FIFO is an actuation; the guarded step – read the status, then act – is the same shape as any other sense-and-actuate. And bytes the FIFO has accepted are actuation already delivered, beyond the store's reach in the same sense as a pin already driven.
What the condition forbids is the HAL layer keeping a copy: a driver that caches the fill level in a variable of its own has created program state below the store – that is the violation, and the remedy is where such bookkeeping always goes: into bridging Components.
Nor is the FIFO draining by itself a breach of passivity: that is the hardware running, and it runs no program code. Only once the FIFO's interrupt is enabled does a hardware event set program activity going, rather than a call from the layers above – and that is precisely the switch from synchronous to asynchronous.
Both conditions relax when the path turns asynchronous. A synchronous bridging unit is a System, so its progress is forced into Components and the HAL beneath it has no program state left to hold.
An asynchronous unit steps off the tick on purpose: its progress cannot be Components – as Data Integrity and Consistency explains – and lives below the store, in the machinery that services the interrupt.
There both conditions give way together: that machinery may carry the interaction's transient progress state, and it runs on its own – set going by a hardware interrupt rather than by a call from the layers above. What is given up is only what the asynchronous choice already gives up – the progress is opaque and unrecoverable, with only the hand-off visible in the store.
Just where that machinery sits is not fixed. The synchronous split is clean because the System forces it – immediate accesses in the HAL, progress in Components above it. Asynchronously nothing forces the split, and the progress-holding machinery can be placed at any depth:
-
interrupt handlers in the bridging layer, over a still-thin HAL;
-
a HAL driver that itself keeps a ring buffer and services the interrupt;
-
the hardware, a FIFO holding the pending bytes in silicon – the same FIFO the synchronous path may use, recategorised: sensed-and-commanded interface-hardware state there, progress-holding machinery here, the enabled interrupt making the difference.
Seen from above the store these are the same program – the progress is opaque and the hand-off is a Component either way – so the bridging-layer/HAL boundary, sharp under synchronous drive, is here a matter of implementation, not of structure. What the architecture fixes is the store boundary; how the machinery below it is layered, it leaves open.
Not all hardware-abstraction code lies on the control path, though. A control program must also bring its platform up – configuring clocks, power, security and partitioning, memory protection – and establish whatever else forms the run-time basis the control loop runs on. This code touches hardware too, but it is not part of the sense-and-actuate path, produces no Components, and stands outside the store's model of the control state; the conditions above do not bind it, and it may hold state and take any form.
The dividing line is role, not peripheral: were the control program to manage power actively, as one of the things it controls, that power interface would move onto the control path and take the conditions on with it.
If you build on an existing driver set – from the silicon vendor, a third party, or your own existing work – rather than write a bespoke HAL yourself, you must ensure that the drivers you place on the control path meet the conditions above.
Values in Stores; Libraries as Mechanics
Two questions recur as a program grows, and this round settled both: where the control program's value data may live, and what a library module may hold.
Control-state values live in the stores. The claim inherits its quantification from Control System and Controlled System: it covers control state, and only that. Outside it stand, each by a rule already made:
-
the technical state behind the handles – living with the modules that operate the devices;
-
the off-the-control-path services and infrastructure, which Layering exempts – a console keeping its writers is unremarkable;
-
the progress of asynchronous bridging units, which the rules place below the store (Layering).
Within its scope, the claim is strict: buffers, counters, port contents, working state of every kind – Components, declared and named by the program, never tucked into the library module that happens to implement the mechanism. A handle is no exception: a stable name resolved elsewhere is itself a value in a store like any other – locked where the wiring is settled for the program's life, an ordinary produced-and-consumed Component where a mode may re-point it at run-time (Partitioning's Coordination has the example). What the criterion governs is when a handle is warranted at all: a handle exists iff a boundary is crossed that raw references must not cross. Two boundaries qualify:
-
genuine pointer resolution: a device record, a writer object, machinery that is technical state by the split above;
-
deliberate denial: the far side of a seam must not name the near side's storage.
Everywhere else the store holds the value directly; a handle that merely looks up a value the store could hold is a layer of machinery to be removed.
Library code contributes mechanics, never ownership. On the control path, a library module is one of two things. A device or service module holds technical state behind a handle – a UART's device record, a writer's descriptor – sanctioned by the split above. Everything else contributes algorithms to store-resident data, under two ownership lines:
-
a library module owns no instances of control state – the instances are the program's, declared in its stores;
-
a library module never decides the program's cardinalities – how many of a thing the program has is always the program's fact.
Capacities are not on those lines – they may legitimately go either way. A library may fix a mechanism's geometry (a sampling port's four slots are the algorithm) or offer sized variants of a type (a buffer in several capacities); the program's authored fact is then the selection, exactly as choosing a peripheral with a given FIFO depth – capacity is the selected mechanism's property, and the selection is program structure. Or the capacity is program-authored outright, through an open parameter. Both conform; what is excluded is only a capacity the program neither selects nor authors – one it could not name or change without patching the library.
How the mechanics reach the program's storage depends on the language:
-
with generics, one library module can be both algorithm and shape: the program instantiates the library's type as a Component – the instance in the store, the parameters drawn from the program's structural facts, the algorithm staying in the library;
-
without generics, the reach happens through signatures, and two kinds of algorithm module compose: state-handling modules carry a protocol over indices and small state records – a ring buffer's head and tail, a sampling port's slot choreography – and never touch the payload, so one module serves every element type (the protocol computes where, the client reads and writes what); data-handling modules bind a payload type to one or more protocols – a print buffer joining a character array to the ring protocol and an event count to the counting protocol. The join is where meaning lives: only the module that owns the payload can translate a protocol's "no space" into an event worth counting. The dependency arrow is fixed – data-handling uses state-handling, never the reverse.
The split has virtues of its own, beyond standing in for generics: a protocol's correctness is proven once, in one payload-free module; payload-blindness is auditable – the module physically cannot touch what its signatures never show it; and each binding is thin and mechanically reviewable. Where generics exist, the split remains a choice on those merits; where they do not, it is the way.
Definitions and Fact Authority
A control program is full of authored facts – sizes, pins, periods, rates, wiring. Where each is defined is architecture, not habit, and one criterion places them all: a definition lives at the level you must consult when it changes.
-
A chip resource assignment – which UART, which pins – is consulted program-wide: it lives in one program-level resources definition. Resources answer which one, never how big: the moment a cardinality appears there, structure has leaked to a level nobody consults for it.
-
A schedule's periods are consulted against the tick of the schedule they pace: they live with the schedule – program-level while one schedule paces the whole program, per schedule where there are several.
-
A structural fact – how many of something exists, which index plays which role – is consulted where the world is created: it lives with the partition that builds that world, never globally. The global variant is not merely untidy but potentially hazardous: a global size silently extends arrays whose new slots no creation code initialises – structure must change loudly, where its consequences are built.
-
Tuning values – rates, thresholds, baudrates, anything changeable without touching structure – live in their own definition module per partition, apart from the structural facts they parameterise.
Three rules run across all of them:
-
fact authority: each value is defined in terms of the module that owns the fact. A pin assignment is written as the LED module's constant, a unit selection as the UART module's – the resources sheet draws from the owning vocabularies and adds only its own choices as plain literals;
-
every fact is authored: nothing is discovered or searched at run-time. There is no "next free" device slot – a chip has the units it has, and which one serves which link is selected, in writing, in the resources sheet;
-
a name asserts an authored choice: a constant earns its name by naming a degree of freedom – which instance plays a role, which unit serves a link – and the name is that choice's identity, valid under any value the choice may take. Where no freedom exists – an index that is pure position, with no role behind it – a name would assert a choice nobody has: the use site carries the literal.
Between partitions, a structural fact can have followers: a peer whose own structure must agree – the coordinator's period-pairs tracking the number of leaders. The authority is unchanged; the follower authors its own copy, deliberately unlinked. Importing the authority's constant would propagate a change silently into structure whose creation code never got the hard look – the same argument that ditched the global definitions, made once more at the peer boundary: a structural change must be loud at every site whose structure it changes. Agreement is a contract, verified rather than propagated: a disagreement surfaces where the shapes first meet, and the silent shapes can be checked at creation by the composition level, which legitimately knows both sides. A counterpart comment at each twin names the other. The pattern is ordinary engineering well beyond software: the plumber relies on the builder to place the wall openings right, each trade working from its own drawings against one agreed design – coordinated plans, not a single automatically propagated one.
None of this is bookkeeping for its own sake. Definitions are the program's earliest interface – the place where a change begins – and placing each with its consulting level and its owning authority is what keeps a change local: one edit, at the level the change conceptually belongs to, with the compiler propagating it everywhere else – and, where a follower exists, a second edit at the follower, deliberately demanded.
Partitioning
Everything so far keeps the whole control system's state in one store. Stated that way, it invites an objection any experienced programmer will raise: the entire program's state in one place, nameable by every System – is that not global data, the thing decades of practice warn against? The objection deserves a direct answer, and it has two parts: what the store actually is, and what becomes of it when the program grows.
The "globals are bad" lesson is about hidden mutable state: variables written from anywhere, aliased, their true dependencies invisible and therefore uncheckable. The store is the opposite on every count. Which Systems read and write each Component is declared; every instance has exactly one writer, and a second one is rejected before the first tick; and a System can name only what its declared dependencies bring into scope, so the dependency list at the head of its source bounds its reach. The properties that make globals dangerous are precisely the ones the store turns into checked declarations.
The mirror image is worth stating just as plainly. State tucked privately into a program's units only looks modular: the dependencies between the units still exist, but they run through call chains and side effects, where nothing can check them. The store does not create that coupling – it makes it explicit and machine-checkable. The precise comparison is not "global versus encapsulated"; it is declared dependencies versus hidden ones.
That answers the discipline half of the objection. The scale half: one store does not mean one store forever.
Partitions
A control system that grows divides into partitions. Each is the unit this document has described all along, complete in itself: its own store, the Systems that run over it, its own structural and tuning definitions, and its own creation code. The division follows the lines such systems divide along anyway – function, team, supplier, certification scope.
Partitioning is a capability, not a mandate. The architecture accounts for partitions and the implementation provides the means – and a program may be exactly one partition, the partition being the whole control program, with the program-level definition modules simply part of it. Nothing below obliges a small program to divide; the rules govern boundaries where they exist.
Where they exist, the first rule decides whether they should: never cut through a mechanism. A good boundary reduces its crossings to declared data – values transported, participation in a facility – and the parties on either side never operate each other's machinery. A bad boundary announces itself by demanding new interface concepts to stay livable: each half of a bisected mechanism needs to reach the other half's state, protocols leak across, and the interface grows machinery of its own. The answer to "this boundary needs a concept" is re-cut the boundary, not invent the concept. Machinery genuinely shared by partitions belongs below them, in the library.
The third-round evaluation program divides along such lines, and the division doubles as a taxonomy worth naming:
-
the plant and its control loop: the print buffer, its drain, the observation path;
-
the demo control application: blinking LEDs under coupled control;
-
mission control: a coordinator sequencing operating parameters;
-
monitoring and test instrumentation: the load generator and the diagnostics.
Four partitions, four reasons to exist – the cut lines are the program's own concerns.
Stores Stay Private; Crossings Are Transports
A partition's store is private: no System of a peer names it. Languages do not generally enforce this – any unit of a program can declare a dependency on any other – but the rule is cheap to hold: a System's reach is bounded by its declared dependencies, and a peer's store appearing among them is the violation, visible in one line of review. What crosses a boundary is a transported value, never a shared store – cross-partition data has transfer semantics, and the architecture gives it exactly two transport shapes:
-
the sampling port – latest-value transfer: one writer publishes, one reader samples; a read returns the newest complete value, re-reads return it again, and a read is defined even before the first write (the port is initialised with an established value). The right shape for measurements, readings, and standing commands – where the freshest value is the one that matters, and missing an intermediate one is correct.
-
the queue – every-value transfer, in order, for streams and events – where each value must arrive exactly once.
The distinction is the same one Crossing the Partition Boundary draws below for the machinery: sample semantics or delivery semantics, chosen by what the data means. Shared state across a boundary, by contrast, would make the two sides co-owners of one picture – which is the definition of being one partition; where that is genuinely wanted, the answer is not to share but to not cut there.
Transports follow two placement rules:
-
Hosting: a transport is hosted by the service-providing side – the collector's intake ring at its collector, the publisher's port at its publisher – and hosted means in the hosting partition's store: the transport is a Component of the host, owned and operated like any other. Hosting is not sharing. The far side participates: it reaches exactly that item, through the host's authored surface (Adapters, next), under the transport's own protocol – each end owns its own accounting, one writer and one reader per end, values crossing whole. That protocol is what makes the one sanctioned reach into a foreign store safe, and it is carried by the transport mechanism itself, never by the callers' good behaviour.
-
Payload authority: the module defining a transport's value type lives where the type's vocabulary lives – in the library when the payload is library vocabulary, in the partition when the payload's shape is the partition's own structural fact. Types cross the boundary; storage does not – what the far side holds in hand is a value and, at most, a handle naming its seam.
Adapters
A partition's own Systems access their store directly. For a foreign System there is exactly one way in: the adapter – a thin, stateless procedure exported by the hosting partition, through which the foreign System reaches one seam's transport. An adapter resolves the hosting store internally – the caller never names it; what the caller holds is at most a handle, published at the interface, selecting the seam – and it exposes exactly the verbs its seam's direction needs: a read seam exports a read, a deposit seam a put, nothing more.
Two constraints keep the idiom sound:
-
an adapter fronts only transports: an adapter over a plain Component would hand a foreign caller undisciplined access to hosted state – the violation the privacy rule exists to prevent, arrived at by another door;
-
an adapter is stateless – a route, not a party: the access it forwards is attributed to the calling System – in the declarations, in the cost accounting, in the evidence – exactly as if the System reached the transport directly. An adapter that accumulated state or logic would stop being a route and become an undeclared System; thinness is not a style preference but the guarantee that the declared picture stays true.
The evaluation program runs both seam directions through this one scheme:
-
on a deposit seam, foreign Systems print through a writer handle published at the hosting partition's interface; each write runs through an installed adapter into the host's buffer ring – a foreign System depositing into hosted storage, safely, because the ring is a transport and the participation is declared in the depositor's own manifest;
-
on a read seam, a consuming System samples a hosted port through a one-verb adapter.
(Within a partition, adapters are unnecessary – a partition's own Systems reach their transports directly; a port between same-partition Systems is a design choice, not a necessity.)
The Seam, Complete
A cross-partition seam is fully specified by three things, each already introduced:
-
the contract – what the two sides agree on: the transported value's type, and any identifying constants. Contracts are bilateral: each side authors its own copy of the agreed constants against the shared understanding, and a disagreement surfaces in the first test run. Deliberately, neither side depends on the other's definition – auto-propagation would make a contract change silent, and a contract change must never be silent;
-
the mechanism – the port, with its transport protocol;
-
the access path – the port's own procedures, or an adapter where the hosting side mediates.
Nothing else crosses. A reviewer can enumerate a partition's entire outward surface by listing its seams, and each seam by its trio.
One Schedule or Several
What a crossing does to the schedule depends on when its consumer reads it. Under one scheduler a port crossing behaves like any declared edge: the derived order spans the partitions, and the partition is purely one of structure. The interesting case is the boundary meant to decouple.
A boundary whose crossings all carry delay tolerance – latest-value samples, buffered streams – decouples the two schedules completely. Each side can then run at its own rate – a fast inner loop here, a slow supervisory loop there – or on its own core, meeting only at the transports. Once the rates differ, "a tick" is no longer common currency across the boundary, and the transports absorb the difference: a queue sized to the ratio of the rates, a sampling port built to be read more or less often than it is written.
The dependency between the two notions runs one way: a schedule cut requires transport-mediated crossings – no order can be derived across independent schedules, so a same-tick edge across the cut cannot exist – but a partition boundary does not require a schedule cut. Under one scheduler the boundary is structure and discipline; the day it must become a schedule cut, its crossings are already the right shape. Drawing partition boundaries along intended schedule cuts is economy, not law.
Consistency coarsens accordingly: the whole control system is consistent where the rates meet; between those points, each partition and each transport is coherent on its own. Data Integrity and Consistency, below, returns to this.
Crossing the Partition Boundary
How a value physically crosses follows the rule the bridging layer set at the hardware: some interactions are immediate, the rest carry progress state. A single word, taken with sample semantics, crosses coherently shared memory in one atomic step – the inter-core analogue of the single pin write. Anything more – a multi-field value, a stream, every-value delivery – takes several steps to cross, and progress state over several steps is bridging-layer work: the transport's own small machinery, a copy at an agreed point.
Note: a single word that merely announces a payload written elsewhere is a multi-word crossing in disguise – the flag and its payload are one value, and must cross as one.
So the machinery at the partition boundary is the bridging layer over again, applied to a partition's seam instead of a peripheral – the same shape, scaled up, and nothing new has to be invented for it.
It follows that the transport behind the seam is an implementation choice, because the Systems on either side see only the transported values. Between two cores of one CPU, the port lives once, in shared memory. Between two CPUs with no shared memory – linked by a CAN bus, say – it lives twice, one replica per side, and the bridging machinery keeps the replicas synchronised: serialise, send, deserialise. The control logic is the same program in both cases, down to the hand-off values; where a partition runs – same core, second core, second box – becomes a deployment decision, not an architectural one.
Two qualifications make that claim precise. The cut must lie along delay-tolerant edges – a same-tick edge across a bus does not exist. And the transport shows through in its quality: a bus adds latency and jitter, which the timing budget must absorb, and unlike a memory write it can fail – so loss and staleness become event sources of their own. Where the control logic should respond to them, they surface as additions to the seam, a freshness or validity value beside the payload: more interface data, decided at design time, never a change of structure.
The data-shape rule pays off once more here: a transported value sized to the transport's natural quantum – one CAN frame, say – crosses atomically in a single send, and the machinery collapses to a one-shot; exceed the quantum, and framing and reassembly progress state appears. The seam behaves the same way at every scale.
None of this is novel, either. It is the partitioned architecture of avionics – ARINC 653's partitions exchanging data through sampling and queueing ports, exactly the two transport shapes above – and the node hierarchy of the synchronous languages. A partition never learns whether its peer runs on the same module or across a backplane. The store brings its discipline inside each partition; the partition structure itself is standard practice.
Coordination
Some concerns cut across partitions: monitoring, operating modes, mission sequencing. Their natural home is a coordinating partition – one whose working material is its peers' seams.
Most such concerns are observational: they read across the seams and write values of their own – health readings, trend estimates. Purely additive; every value still has its one writer.
The rest command: a mode change or a mission step alters how the control partitions behave. In store terms this is not an intervention but an ordinary edge: the coordinator produces policy values – set-points, gains, selectors – and the control Systems consume them, across a port like any seam. No second writer appears; the policy values belong to the coordinator by design.
Commands come in two species, distinguished by who owns their correctness:
-
command as value – the commander knows the commanded side's semantics and ships operational values (a period, a set-point). The value set may well be constrained – a period a multiple of the tick, a set-point within bounds – and the receiver checks such declared constraints; what it cannot check is whether the value serves its purpose. That judgment, and with it the correctness burden, rides with the commander.
-
command as reference – the commanded side owns an authored repertoire of configurations, and what crosses is an index into it. The commander knows the repertoire's command index values, never its contents; correctness lives entirely with the commanded side, and an invalid configuration is not rejected but unrepresentable – one membership check is complete validation. This is the certifiable shape: the mode table.
The criterion is ownership, not the shape of the value set – a discrete, constrained set can still cross as values. Reference, where the commanded side can author the complete set of valid configurations; value, where the commander must express choices the commanded side has not enumerated. And a naming smell locates misplaced ownership: when a table's value names speak the commanded side's domain but the table lives with the commander, domain knowledge has migrated across the seam – the reference form brings it home.
Note what a mode switch does not do: it changes values, never structure. The Entities, Components, and Systems are the same before and after; a new phase is a new set of numbers flowing through a structurally unchanged store. The fixed-shape property of The Concepts is exactly what makes reconfiguration safe to reason about – and how far it can reach is fixed before the first tick: everything is allocated at creation, a mode switch selects among resources that already exist, and can reach nothing that was not provided for.
Positioning: Synchronous Islands
The partitioned picture has a name in the literature: GALS – globally asynchronous, locally synchronous. Each partition is a synchronous island: a static schedule derived before the first tick, values with hold semantics for free (a Component keeps its value until its producer runs again), the previous-value mode supplying the unit delay – the properties the synchronous languages define, arising here from persistence, run-to-completion, and the derived order. The seams between islands are the asynchronous part: real concurrency, confined to declared crossings with declared transports. What the synchronous tradition derives by compilation, this architecture authors and checks – a difference in method over the same shape.
Data Integrity and Consistency
The Motivation asked for one thing before all others: a valid picture of the control system's state, coherent and readable at the moment it is needed. The chapters since have built structure on that demand. This one states the result precisely – when the store can be trusted, why the guarantee holds with nothing defending it at run-time, and what it makes possible beyond recovery.
Two Consistency Points
The strongest statement first. At the tick boundary – all of a tick's Systems have run to completion, the next tick's have not begun – the store is one coherent snapshot: every Component carries a completed value of this cycle, none is part-way through an update. That snapshot stands until the next tick begins.
The tick is not an opaque interval, though. The store advances System by System, in the derived order, each running to completion – so there is a second, finer consistency point at each System's completion: everything that System produced is whole from that moment on. A consumer can never meet a torn value; its producer either completed earlier in the same tick, or the value stands whole from the tick before.
Nothing defends any of this while the program runs – no lock, no critical section, no atomic instruction on the hot path. The guarantee is structural: Systems are cooperative and run one at a time to completion, every Component instance has one writer, and the order is derived from the declared edges before the first tick. Two accesses to the same Component are never concurrent because no two Systems ever run at once. The schedule is the mutual exclusion – checked at construction, not enforced at run-time.
The contract has a second half, and it binds the Systems. Cooperative scheduling grants each System the store to itself; in return, every System must run to completion promptly and give the processor back. Busy-waiting inside a System is therefore an absolute no-no. A System that spins on a condition – a transmitter ready, a device's answer, a flag from a peer – holds not just the store but the whole tick: no other System runs, and the schedule now stretches on an event outside the program's control. It also forfeits the analysable cost that closed What This Means for a Control Program – a spin has no fixed count.
A System checks, acts on what is ready, and moves on; what is not ready this tick is next tick's work, or the bridging layer's. The architecture has exactly one place to wait – between ticks – and no System ever waits inside one.
What It Buys
Recovery first, since it was the motivation. The picture to recover from is the store at the last consistency point – present by construction, every tick, with no quiescing, no coordination protocol, no shadow copy maintained by discipline. What the Motivation asked for, the structure simply provides.
Testing and simulation come with it. A System is a function from store to store: set up Component values, run it, read the result – no scaffolding, no mocking apparatus. Replace the sensing Systems with ones that produce recorded or synthetic values, and the whole control logic runs unchanged against a simulated plant. Record the store at tick boundaries, and a run can be replayed and examined tick by tick.
Monitoring is the same property read continuously: an observer that runs at the consistency points sees coherent values without stopping anything. The observational coordination of Partitioning rests on exactly this – and the Observations set builds its whole evidence machinery on it.
The Asynchronous Seam
Layering deferred one question here: why an asynchronous unit's progress cannot be Components. The answer is now short. Everything above rests on one condition – the store changes only inside scheduled Systems, one at a time. An interrupt handler runs at hardware time: mid-tick, mid-System, whenever the hardware signals. Were it to write Components, the store would change underneath a running System, and both consistency points would dissolve. State driven by the hardware's timing therefore lives below the store, in the bridging machinery – opaque and outside the recoverable picture, the price Layering already put on the asynchronous choice.
The two sides still have to meet, and the bridge is built so the guarantee survives it. On the store side stands a System – scheduled, run to completion, the only party that touches Components. On the far side, the asynchronous unit. Between them sits a buffer or queue shaped for exactly this crossing: one producer, one consumer, each side owning its own end of the accounting and writing nothing of the other's, values crossing whole, never part-written.
One case remains: the queue is full – the consuming side has not kept up, whichever side that is: the handler on the way out, the System on the way in. Neither side may wait for room: a System never waits, and an interrupt handler cannot.
So each crossing carries its own drop policy: drop the newest (refuse the excess), or drop the oldest and keep the latest, which for measurements is usually the right choice, the freshest sample being the one that matters. Either way the drop is recorded, not swallowed: a count reaches the store as a Component like any other, and loss at the seam becomes ordinary control data – an event the control logic can observe, weigh, and respond to, like anything arriving from the plant.
Across Partitions
Partitioning left one thread here. Under one scheduler nothing changes: one tick, one derived order spanning the partitions, the same two consistency points. Under several schedulers the guarantee retreats in an orderly way rather than collapsing. Each partition keeps both consistency points at its own tick; each crossing stays coherent through its transport; and the control system as a whole is simultaneous only where the rates meet. Recovery and monitoring take the same shape – per-partition pictures at their own tick boundaries, a global picture at the meeting points.
The RTOS Question
A program built this way needs remarkably little from the platform beneath it: a loop that runs the Systems in their derived order, a tick to pace it, and the interrupt machinery of the bridging layer. That is the whole scheduler.
An RTOS can certainly host it, and often will, for reasons that have nothing to do with architecture – licensing and certification artefacts, company policy, existing investment, plain familiarity. Nothing above forbids that.
RTOS is a wide family – from time-triggered minimalists such as OSEKtime, through kernels that can be run cooperatively, FreeRTOS and Zephyr among them, to the decidedly more comprehensive, VxWorks and its kin. Which of them, and in which configuration, matters less than it seems. As with the driver set at the end of Layering, the question is not the feature list; it is the conditions that whatever runs over the store must meet. For the consistency guarantee of Data Integrity and Consistency to survive, the tasks must satisfy:
-
they are stateless – a task is a vehicle for Systems, nothing more; it carries no control state of its own from one activation to the next, and everything it works on lives in Components, accessed under the declared producer and consumer roles;
-
they run through – an activation runs its Systems to completion and ends; no mid-execution yield, no blocking call inside a System. Control leaves a System only at its completion – the voluntary yield and the involuntary pre-emption break the same guarantee;
-
they map to the structure – either one task per partition, running all of that partition's Systems in the derived order (task boundaries on partition boundaries; pre-emption between such tasks is pre-emption between partitions, which the architecture already accommodates); or one task per System, activated on a fixed timing roster that reproduces the derived order. Either way, two tasks over the same store must never overlap;
-
the task set is fixed – tasks are created at world creation and none is created or destroyed at run-time; the roster is structure, and structure does not change;
-
they communicate only through the store – no task-to-task messages, event flags, semaphores, or side variables; the RTOS's queues have exactly two legitimate places: as the transport at a partition boundary, and at the asynchronous seam;
-
interrupt-servicing tasks are bridging machinery – where the RTOS defers interrupt handling to a dedicated task, as most can, that task stands where the interrupt handler stood: an asynchronous bridging unit, not a System. Its state stays below the store, and it meets the store only across the seam.
One test compresses the list: the store never needs a lock. The mutexes and priority ceilings an RTOS carries exist to defend shared state against concurrent access – a problem the conditions above have already removed. If a lock around a Component ever seems necessary, a condition has been broken, and the lock would not repair it, only conceal it. Held to these conditions, the RTOS – minimal or comprehensive – is one more platform the structure rides unchanged.
World Creation and Steady State
Everything so far has taken a running program as given – platform up, hardware configured, store built and valued. This chapter looks at how it gets there. World creation is named for what it creates: everything – the whole world the program runs on and through, from the infrastructure to the store's initial values – and it spans reset to the first tick. A program in this architecture lives in two phases, and the line between them carries as much weight as any layer line drawn earlier. In a partitioned program, creation runs per partition – each partition creates its own world, the partitions' creations mutually independent – under a thin program-level sequence that brings the platform up first and starts the tick last.
The Two Phases
World creation runs once, and it opens with its own foundation: the first sub-phase is the program start-up, in this framework an established concept with its own implementation (program start-up). It brings the infrastructure up: clocks running and locked, power and memory configured, the run-time basis in place (the off-the-control-path code of Layering), all of it verified.
Its contract is the infrastructure invariant: everything after assumes it, no System ever configures it (a monitor may observe it), and its only re-establishment is the reset – world creation again, from the top. The infrastructure does not feed the control laws; it makes them valid – a mistuned clock does not disturb a PID computation, it falsifies it.
From that basis, creation is ordinary sequential code, and it enjoys every freedom steady state will lack: it allocates, it configures the interface hardware the bindings name, and it may even wait – for a device to answer, for a peripheral to come out of reset. Waiting, forbidden in a System, is unremarkable here; the tick has not begun.
Steady state is everything from the first tick on – the regime this document has described all along. Here the rules bind: all state in Components, Systems run to completion, no waiting, and no allocation, ever. Values change; structure does not.
For world creation, the remaining sub-phases can be identified with a natural inner order – each defined by its contract, by what must hold when it completes; the mechanics of meeting it are the program's business:
-
build – the store and the interface hardware reach their structural state: every Component instance in place, bound and connected, the hardware configured as far as it does not depend on Component values. Configuring starts from a defined baseline, which build establishes rather than assumes: a hardware reset, where the start came from one, supplies it for the on-chip peripherals; whatever it does not cover – a device on a bus that held its state, a start that was not from reset at all – build brings to the baseline itself. How – allocation, wiring, construction calls – is not prescribed; it varies with the storage model, the language, and the program. Contract: acquire can run.
-
acquire – give every Component that represents a measurement an admissible starting point. The default is a designed initial value the consuming algorithms handle correctly – a filter prior, a rate of zero – with a bounded, acceptable transient until real samples arrive, from the first tick on. Where the transient matters, take a first, real sample instead: a stronger source, and the basis of a bumpless start. And where the found state cannot be measured, or is not admissible to start from, establish the value: command the controlled system into a defined state first – drive an axis to its reference stop, sample under a defined excitation – and the value is known, by construction or by a sample taken under the established conditions. Such forcing is creation-time actuation, the same freedom as creation-time waiting, and what it commands on the way is scaffolding – superseded when value and propagate close the sequence. An explicit not yet measured marking belongs only where a validity Component exists anyway, maintained for run-time reasons – introduced solely for start-up, it would put a permanent check on the hot path and hand initialisation to the Systems. Contract: every measured Component holds an admissible value, its consequences designed for. (Where acquire samples, the sampling doubles as the live check: a sensor that cannot deliver fails creation, in the phase where failure is allowed and handled – and an established value is the strongest check of all, exercising the actuation path as well as the sensing path.)
-
value – set every remaining Component to its initial state; initial values may build on the acquired measurements – starting a controller at the actuator's actual position, say. Locked values may be derived as well as copied: where authored inputs determine a set of locked results, a library algorithm computes them here, once, with its validity checks run as creation-time assertions – a declaration that fails them refuses the world before the first tick. Contract: the store is complete and valid – the zeroth consistency point holds.
-
propagate – write the store's commanded values out to the hardware, so that its initial state is derived from the store, never stated independently: one set point, and a mechanical write-out – decision-free, and final: it supersedes whatever creation commanded on the way to an established value. Contract: the interface hardware agrees with the store – a condition on the endpoint, not on the path.
Each sub-phase builds on the contract of the one before, and by the end nothing remains to be done but to start the tick.
Note that using an RTOS as the implementation substrate shifts the infrastructure base, and may impact world creation.
What Creation Must Deliver
Three obligations, and steady state depends on each.
The structure, complete. All allocation happens here – including everything any runtime reconfiguration may ever reach. The reserve device of Partitioning's Coordination is allocated, configured, and held ready now; the mode a mission step will switch to exists now. The set of configurations the control system can ever be in is enumerated by what creation builds: steady state selects among them, it never adds to them.
The structure, verified. The declared roles are checked and the run order derived here, before the first tick. An instance with two writers, a circular dependency, a deposit nobody collects – the program is rejected on the spot, at construction, not discovered mid-flight. Structural invariants are creation's business alone; steady state never re-examines them. What remains on the hot path is only what genuinely varies at run-time – the guard on a queue, the check on a value arriving from outside – never a re-proof of the design.
The values, coherent – on both sides. The first tick must find a complete, valid initial state: every Component carrying a meaningful value from the start, and the interface hardware in agreement with the store: the commanded side written out from the store's values, the measured side holding admissible starting points – sampled or designed.
Systems play no part in any of this. They implement steady state – and initialisation left to them would rest on their run order, which is derived from the declared roles and may legitimately change as the control system evolves. That kind of argument, correctness resting on which piece of code happens to run first, is exactly what the store is meant to replace. Robustness rests on data invariants instead: the store is valid at every consistency point, including the zeroth, before the first System has run – and the hardware agrees with it.
One Way
The switch between the phases is a single, sharp moment: creation finishes, its checks have passed, the scheduler starts the tick – and there is no way back. Nothing in steady state re-enters creation; the only return is the full reset of the Motivation, which is creation again, from the top.
The switch deserves enforcement, not only convention. Every resource that start-up and creation configure can offer a lock: the clock tree frozen already at start-up, the memory allocator locked, timer and channel assignments sealed – and the store's own structure frozen last, the same gesture applied to the store itself. After its lock, a stray allocation or reconfiguration call does not fail quietly in the field, under conditions no test reproduced; it faults immediately, in testing, at the point of the violation. Discipline becomes a testable guarantee.
Not everything that can be locked should be, though; the lock follows the dividing line Layering drew – role, not peripheral. What is settled for the program's life – infrastructure, wiring, the platform's shape – is locked. What the control loop itself commands is not configuration, however much it resembles it: a programmable voltage, a gain, a duty cycle written through a peripheral's registers is actuation, driven from Components, and a lock bit engaged over it would turn the next control action into a fault.
One peripheral may sit on both sides at once – its bus and pin configuration locked, its output register the control loop's actuator. The line is drawn at design time, resource by resource: locked, because no correct program touches it again; or open, because steady state owns it – through the store, like everything else it does.
Where the platform offers real enforcement – a peripheral's configuration lock bit, a security partition – the lock engages it; where it does not, a checked flag still catches what happens in practice: not hostile register-poking, but the accidental reconfiguration call in a path that should never run in steady state. And the run of lock calls that closes creation makes the phase switch explicit and auditable. Everything before them is configuration; everything after is steady state; asked where the transition lies, the program can point to the line.
Everything dynamic this document has allowed since lives strictly on the steady-state side of that line, working on what creation provided. Reconfiguration selects among prepared alternatives; the coordinator re-points bindings to devices held in reserve; recovery – the motivation for the whole architecture – re-values Components and re-establishes agreement, propagating the commanded side out and re-acquiring the measured side in, without allocating anything. Nothing can appear at run-time that creation did not build. The store's shape, the schedule, the memory footprint: all are settled before the first tick – which is what the hard-real-time claim of The Concepts quietly rested on all along.
Method
One meta-rule ran through the round that settled this edition, and it belongs in the record because it generates rules rather than merely collecting them: claim everything the compiler can enforce, along the structure's real seams. Where knowledge is authored – a size, a role, a type – the check belongs at the same place, in the strongest form the language offers: a distinct named type, so the type checker rejects a mis-wiring; an item kept out of reach, so visibility itself is the guarantee; a creation-time assertion where a relation must hold. "Checks live where the knowledge lives" – the check gradient follows the knowledge gradient, and a check placed away from its knowledge is a convention waiting to drift.
Its companion is improvement by reduction: the architecture advanced this round mostly by removing machinery once its job was understood – lookup layers dissolved into direct values, ceremonial record wrappers collapsed where an enclosing type already carried the check, generic interfaces demoted to authoring templates, agreement checks eliminated by giving every fact a single authority. Each removal made the remaining structure claim more with less. Where a concept seems to need scaffolding to stay livable, the scaffolding is usually the message.
The Invariants
Each chapter has added rules; collected, they are few, and they are all one move. Every rule either pushes a decision into creation or keeps a surprise out of steady state – so that what runs, tick after tick, is a fixed structure carrying changing values, with nothing left to vary but the values themselves. They come in two kinds.
Most are structural invariants: properties of the program – settled once, before the first tick, and never re-examined at run-time; how each one is held (language, gate, review) closes this chapter. Stated once, together:
-
All state is in Components; a System holds none. The store is the complete picture – recovery's starting point, testing's fixture, monitoring's window. (The Concepts)
-
Every Component instance has exactly one writer. A second writer is rejected before the first tick. (The Concepts, Dataflow Roles)
-
The run order is derived from the declared roles. Nobody authors it: the declared footprints alone determine it, and a dependency cycle – an order that cannot exist – is rejected before the first tick. (Dataflow Roles, Data Integrity and Consistency)
-
A shared facility has its declared depositors and at least one collector. In the indexed kind, depositing is writing one's own slot – the single-writer invariant over again; in the stream kind, the deposits share one transport, each whole by run-to-completion, never concurrent under the schedule. Every collector runs after every depositor, and a deposit nobody collects is rejected before the first tick. (Dataflow Roles)
-
Systems meet only in the store. No System calls another; no task or partition communicates beside it. (The Concepts, Partitioning)
-
A System runs to completion. It never busy-waits, blocks, or yields; the architecture's one place to wait is between ticks. (Data Integrity and Consistency)
-
The store holds values and handles, never references to data via pointers or addresses. A snapshot of values is a state; a snapshot of an address is not. A hosted transport's protocol state is values like any other – the protocol itself is the library's. The technical state behind the handles lives with the modules that operate the devices, and is re-established beneath unchanged handles – a handle is the sanctioned indirection because it is an index, not an address, and it exists only where a boundary is crossed that raw references must not cross. (Control System and Controlled System, Values in Stores)
-
Library code contributes mechanics, never ownership. A device module's state is technical, behind a handle; no library module owns an instance of control state, and none decides the program's cardinalities; note that capacities may be library-fixed, the program then is authoring the selection. (Values in Stores; Libraries as Mechanics)
-
Every fact has one authority. A definition lives at the level consulted when it changes, expressed in the vocabulary of the module that owns it. Within the authority's reach, second authorities are not checked but eliminated; across partitions, a peer whose structure must agree authors its own copy against the contract – agreement is verified, never silently propagated. (Definitions and Fact Authority)
-
On the synchronous control path, the HAL is stateless and passive. The interface hardware's state is its own, not the program's: sensed fresh, guarded, never cached in software below the store. (Layering)
-
Asynchronous units are not Systems. Their progress lives below the store; they meet it only across the seam, and what the crossing loses returns as ordinary data.[1] (Layering, Data Integrity and Consistency)
-
Stores are partition-private; partitions meet only through transports. Hosting is not sharing: a transport is a Component of the hosting partition's store, and it is the only item a peer can reach – through the authored surface, under the transport's protocol. The rest of the store is out of a peer's reach entirely; state data crosses only by being published into a transport. Cross-partition data has transfer semantics – a sampled latest value or a queued stream; a seam is its contract, its mechanism, and its access path. Independent scheduling happens only across such crossings. (Partitioning)
-
Every store access is a System's. In steady state the store has no other reader or writer: the kernel runs the Systems but touches no Component; library code operates only within a System's call, on what the System hands it; an adapter is a route, not a party – stateless, verb-restricted to its seam, attributing everything it forwards to the calling System. Access is thereby attributable without remainder: whatever machinery sits on the path, the access belongs to a System. (Creation code writes the store before any System runs – this invariant governs steady state.) (Partitioning, World Creation and Steady State)
-
Structure is fixed at creation; steady state changes values. Every resource any configuration will ever need is allocated, configured, verified, and initially valued before the first tick – and locked there. (World Creation and Steady State)
Two more are invariants of a different kind – operational invariants: properties of the physical, established and verified at creation, holding in operation – and violable by physics, which no construction can prevent:
-
The infrastructure is correct. Established, verified, and locked by the program start-up, world creation's first sub-phase; no System configures it. (World Creation and Steady State)
-
The interface hardware is functional. Verified at creation by live checks – reading a sensor, exercising a link; where acquire takes a first sample, the sampling is the check. A device that cannot answer fails creation – in the phase where failure is allowed and handled.
The Systems for the control logic run as if both hold, probing nothing on their path – and in a perfect world, they do hold. The real world can violate them, and what separates a real system from a perfect-world one – or from a simple test program, which may legitimately live on assumption alone – is supervision.
Its evidence comes from two sources:
-
dedicated Systems running live checks periodically, where a probe is safe and meaningful;
-
the outcomes the using Systems record anyway – most devices reveal a fault only in use, as a failed read or an absent answer, and the System using them writes what it experienced, the value and its quality, as Components.
Either way, health reaches the control logic as data, and a control logic System's run completes on imperfect input with designed behaviour – hold, last-good, degrade. Which operational invariants earn a monitor is a risk decision, the damage of a violation weighed against its probability – so the path from test program to hardened system is an ordered list of monitors, not a rewrite. How watching over time is done properly – tolerance declared, detection calibrated, response laddered – is the Observations set's subject, built entirely from the elements of this document.
The two differ in their violation response: a failing sensor is a disturbance or a recovery case – degrade, switch to a reserve; failing infrastructure gates everything, because recovery itself runs on it – trust it, or reset. (In Design-by-Contract terms, the operational invariants are the ordinary, violable run-time kind; the structural ones are the stronger claim.)
The structural rules ask for no run-time defence: some are checked at construction – the single writer, the acyclic graph; some are enforced by locks – allocation, configuration, the structure itself; some are enforced by the language – distinct types, restricted visibility, read-only access; the rest are visible in review. The operational ones are supervised, not defended: health arrives as data, never as probing on the hot path. And every rule protects the same asset: a store that can be trusted at every consistency point.
A control system built to these rules holds no surprises at run-time, because every surprise was either resolved at creation or excluded by design. This is the principle of no surprise, already standing in this framework's start-up design, and its formulation there holds here unchanged: let the surprises be in the controlled system, not the control system. The controlled system supplies uncertainty enough; the program's own resource picture is the one place it can be designed out entirely. Which is where this document began: the state you would recover from is, at every tick, simply the state you are running on.
See Also
- ECS for Control: Implementation
- ECS for Control: Observations – Concepts
- ECS for Control: Observations – Calibration
- ECS for Control: Observations – Implementation
- ECS for Control: Observations – Verification
Marked for possible extension: the transport protocols serve an unsynchronized peer by design, and an interrupt-driven unit is exactly such a peer, paced by hardware rather than by a tick – treated as a System of its own schedule, with its port hosted in the store and its production declared, this seam would fold into the partition crossing. A later round evaluates that; until then, this rule stands. ↩︎
Last updated: 27 August 2026