Oberon RTK

ECS for Control — Concepts and Motivation

Basics of using an Entity Component System architecture for real-time control programs.

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, where it earned its keep for reasons that have little bearing on real-time control. What makes it worth examining here is that, set apart from those origins, its 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. This document is an assessment of that fit: what ECS is, and what its structure means for a real-time control program.

Evaluation Programs

The concepts described here are explored in a series of evaluation programs. Two more documents complete this initial evaluation: the lessons learned so far, and an outlook on the next steps.

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 a set of Component types and, each tick, runs over their instances; an Entity groups one instance of each of the types that describe it. 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. Each Component type is written by exactly one System – its producer, which writes every instance of the type – and read by other Systems, its consumers. Because these roles attach to the types, the order the Systems must run in can be derived, a circular dependency detected, and a second writer to a Component type 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.

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 adaptation 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.

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.

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
-----------------------------------------------------------
adaptation 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.

Adaptation Layer

Between the control logic and the hardware sits 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 adapt 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 adaptation layer's work. So an LED, set by a single pin write, needs no adaptation; the terminal, fed a report the UART emits over many ticks, does.

Three ways to run it. The adaptation 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 adaptation 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 adaptation layer.

What surfaces as a Component. Which of an interaction's state appears in the store depends on how it is adapted. A synchronous adaptation 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 adaptation 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;

  • adaptation Components – the working state a synchronous adaptation 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 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 adaptation 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 adaptation 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 adaptation 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 adaptation 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 adaptation 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 adaptation 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 adaptation 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 adaptation-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.

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 a Component type is declared; each type has exactly one producer, and a second one is rejected before the first tick; and a System can name only what lies in the stores it imports, so the import list at the top 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.

Control Sub-systems

A large control system divides into control sub-systems. Each is the unit this document has described all along, complete in itself: a Component store, the Systems that run over it, and the interface Components it is wired to. The division follows the lines such systems divide along anyway – function, team, supplier, certification scope.

One discipline makes the division worth having: peers stay private, and meet only in the interface. A sub-system's internal Components are its implementation; no System of a peer may reach them. What it shares, it shares by producing and consuming Components in a small store both sides are wired to – the interface store, its published interface. Two peers never see each other; they see the interface store between them: the shared data sitting on the partition boundary that separates them.

The unit is recursive: sub-systems compose into larger sub-systems through shared interface stores, and the whole control system is the top of that composition. A sub-system may be party to several interfaces, so the arrangement is a graph, not a tree. And the meeting place should be sized to the interaction: a plain hand-off between two parties needs only the shared Components themselves, while an interaction with genuine computation of its own – sensor fusion, arbitration, coordinate transforms – earns a control sub-system in its own right, one whose domain is the coordination.

Partitioning changes none of the rules. Single-producer holds across stores – one producer per interface Component type, wherever it lives – and the run order is still derived, now spanning stores. The hierarchy is organisational; the semantics stay global.

One Schedule or Several

What an interface Component does to the schedule depends on when its consumer reads it. Consumed in the same tick it is produced, it orders the producer's sub-system ahead of the consumer's: the two remain under one tick and one derived schedule, and the partition is purely one of structure. Consumed only later – the next tick, or any tick after that – it imposes no order within the tick at all; and that is the interesting case.

A partition boundary whose interface Components all cross with delay 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 buffered interface. Once the rates differ, "a tick" is no longer even common currency across the boundary, and the delay's actual span is absorbed by the interface Components themselves: a queue sized to the ratio of the two rates, or a sampled latest-value built to be read more or less often than it is written. Such boundaries are the natural cut lines: draw the partitions so that the parts meant to be scheduled independently meet only across delayed, buffered interface Components.

Consistency coarsens accordingly: the whole control system is consistent where the rates meet; between those points, each interface store is coherent on its own. Data Integrity and Consistency, below, returns to this.

Crossing the Partition Boundary

How a value actually crosses follows the rule the adaptation layer set at the hardware: some interactions are immediate, the rest carry progress state. A single word, taken with sample semantics – the consumer reads the latest value, and may read the same one twice or miss one – crosses coherently shared memory in one atomic step and can go direct: the inter-core analogue of the single pin write. Anything more – a multi-field Component, a stream, every value delivered exactly once – takes several steps to cross, and progress state over several steps is adaptation-layer work: a double buffer, a small queue, a copy at an agreed point.

One trap: 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 adaptation layer over again, applied to a sub-system's interface instead of a peripheral – the same seam, 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 interface Components. Between two cores of one CPU, the interface store can live 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 adaptation keeps the replicas synchronised: serialise, send, deserialise. The control logic is the same program in both cases, down to the hand-off Components; where a sub-system 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 delayed 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 disturbance sources of their own. Where the control logic should respond to them, they surface as additions to the interface, a freshness or validity Component beside the payload: more interface data, decided at design time, never a change of structure.

The data-shape rule pays off once more here: an interface Component 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, the sampled latest-value and the queued every-value of the split 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 every partition: monitoring, fault detection, operating modes, mission sequencing. Their natural home is a coordination sub-system – one whose working material is its peers' published interfaces.

Most such concerns are observational: they read across the interfaces and write Components of their own – health flags, disturbance counters, trend estimates. Purely additive; every Component type still has its one producer.

The rest reconfigure: a mode change or a mission step alters how the control sub-systems behave. In store terms this is not an intervention but an ordinary edge: the coordinator produces policy (or configuration) Components – setpoints, gains, mode selectors – and the control Systems consume them. No second writer appears; the policy values simply belong to the coordinator by design, rather than to the Systems they steer.

And 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 as described in The Concepts is exactly what makes reconfiguration safe to reason about. What it costs lives in the control laws themselves – switching without transients, taking over smoothly – not in the architecture.

How far a reconfiguration can reach is fixed just as firmly, before the first tick. Everything is allocated once, at creation (World Creation and Steady State, below), and nothing new comes into being on the fly: a mode switch selects among resources that already exist, and can reach nothing that was not provided for.

If, say, output is to move to another UART, the binding through which the device is reached can be re-pointed – a value change like any other, the binding now one more policy Component owned by the coordinator – but only to a device that was allocated, configured, and held in reserve at world creation. Every configuration the coordinator can switch the control system into was enumerated there; a mode switch chooses from that set, and never extends it.

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, each Component type has one producer, 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 adaptation 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.

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 adaptation 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 – a disturbance the control logic can observe, weigh, and respond to, like any disturbance arriving from the plant.

Across Partitions

Partitioning left one thread here. Under one scheduler nothing changes: one tick, one derived order spanning the stores, 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 interface store stays coherent through its crossing machinery; 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 adaptation 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 crossing machinery at a partition boundary, and at the asynchronous seam;

  • interrupt-servicing tasks are adaptation machinery – where the RTOS defers interrupt handling to a dedicated task, as most can, that task stands where the interrupt handler stood: an asynchronous adaptation 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.

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. 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: the strongest source, and the basis of a bumpless start. 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.)

  • 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. 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. Contract: the interface hardware agrees with the store.

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 producer and consumer roles are checked and the run order derived here, before the first tick. A Component with two producers, a circular dependency – 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.

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, guaranteed by construction – a correct build cannot violate them, so they are checked once, at construction, and never again. 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 type has exactly one producer, and the run order is derived from the declared roles – a second producer or a dependency cycle is rejected before the first tick. (The Concepts, Data Integrity and Consistency)

  • 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)

  • 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. (Layering, Data Integrity and Consistency)

  • Partitions stay private and meet only in interface stores. Independent scheduling happens only across delayed crossings. (Partitioning)

  • 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.

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 producer, the acyclic graph; some are enforced by locks – allocation, configuration, the structure itself; 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 construction. 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.

Reading On

Last updated: 13 July 2026