Skip to content

Repository files navigation

Eclipse ThreadX ZoneX

ZoneX is a deterministic partitioning hypervisor for mixed-criticality embedded systems, and a component of the Eclipse ThreadX suite.

It runs at EL2 on Armv8-R, gives each partition a statically declared slice of memory and of time, and treats a partition stepping outside either as a fault to be reported rather than a condition to be recovered from.

Status: Phase 0, under construction — a demonstrator that now time-partitions

This is not production software and is not close to it. It is a demonstrator, built to establish that a small set of mechanisms work on real Armv8-R silicon and to measure what they cost. Read the numbers below as a first measurement on one bench, not as characterisation.

What it does today. Two ThreadX kernels run at EL1, each confined to its own stage-2 window, time-sharing one logical core under a static major frame taken from a manifest — on the Armv8-R AEM FVP and on the S32Z280-594EVB. Partition A holds seven ticks of every ten and partition B three, and each partition's clock advances by its own windows and by nothing else.

And that is now asserted rather than shown. A regression sweeps fourteen isolation cases in one run — seven in each direction, each aimed at an address of its own — and measures the critical partition's window period continuously while the untrusted one is steered through five behaviours: idle, computing, computing with its own interrupts masked, storming the console, and violating its boundary on every iteration of its own loop.

On the board A's window period is 800,000 counts of an 8 MHz counter. Over six hundred major frames it moves by 22 counts while its neighbour idles, 20 while that neighbour computes, 24 while it computes with IRQ and FIQ masked, and 185 while it commits a hundred and three thousand boundary violations.

An independent run six days earlier agreed to within the scatter — 21, 21, 22 and 304 counts, with the same 22-byte console hypercall — so these are a reproduced measurement rather than one run's luck.

One thing does reach a neighbour, and it is the hypervisor's own doing. In the same run, the phase where the untrusted partition storms the console moves A's period by 17,951 counts.

Nothing a partition does through the schedule reaches its neighbour — the figures above are tens of counts, against a violation count in six figures. What reaches it is ZoneX's console driver. A guest's console is one hypercall per character, answered at EL2 with PSTATE.F set, so the FIQ that ends a window waits for it. Nearly every one of those hypercalls writes the single byte the guest asked for. The one that opens a line writes twenty-two — the newline a deferred close still owed, the tag naming the partition, and the guest's own character — and that is 106,116 core cycles, 2.2 ms, about 17,640 counts, with the boundary interrupt held off throughout.

That is a defect in ZoneX, not a limit of the partitioning, and it is bounded, derived and reproducible. The bound is one line tag: a period is a difference between two window entries, so a constant deferral cancels in it and only a change reaches the number — one long period and one short correction, 35,280 counts against a half-window bound of 40,000. Every other phase is held to one eighth of a window.

It is also not rare, and that took finding out. Runs that once looked clean were runs where the phase relationship between a fixed schedule and a guest printing a fixed message happened to keep the boundary out of a tag; adding two cycle-counter reads per character, under one per cent of a character time, moved that phase and the excursion appeared on every run. What removes it is a console the hypervisor can hand a byte to without waiting for the wire — an interrupt-driven driver with a polled fallback the fault path can force, because the fault reporter prints at the moment ZoneX has already failed once. That is named and costed in docs/wcet-inputs.md, and it is not done.

Three results, and these are mechanisms rather than measurements — they do not move when the numbers below do:

  • A partition cannot reach its neighbour's memory, even after granting itself that memory in its own EL1 MPU. Stage 2 refuses, the offender is stopped, and the other partition runs to the end of the frame with its schedule untouched. Being attacked costs the neighbour nothing.
  • A window ends whether the partition agrees or not. The hypervisor's timer sits in GIC Group 0, so it arrives as an FIQ at EL2, and with HCR.FMO set PSTATE.F is ignored at EL0 and EL1. One build proves it by trying: a partition that masks IRQ and FIQ and spins for ever is preempted exactly on schedule, and all its masking costs it is its own kernel's tick.
  • Each partition's clock is its own, and advances in its own windows and in nobody else's.

Measured on the board over twenty frames and thirty-nine window boundaries, with no missed deadline:

counts on the core its own ticks
partition A 11,164,192 139
partition B 4,818,043 51

A × 3 = 33,492,576 against B × 7 = 33,726,301 — within 0.7% of exact. A partition able to see wall clock would be out by a factor of three, not by a percentage. This one is a ratio of two readings of the system counter, whose frequency was established three independent ways, so it does not depend on the core clock, the caches or the optimisation level.

And one measurement, which will change. A partition switch costs about 6,000 cycles on the S32Z280 — 5,662 / 5,703 / 5,982 min / mean / max on the most recent run, with the mean spanning 5,703 to 6,078 across six readings taken on this bench over a week. The guest's own EL1 MPU is 3,407 of those 5,703 — 85% of the save and restore, and 60% of the whole switch. A switch is not expensive because the hypervisor does much; its per-partition state is three register writes. It is expensive because a guest has a lot of registers, and most of them are its memory protection unit.

Read that as ±4%, not as four significant figures. The same code re-measured a day later, on the same bench, reads 3% higher — and the two reasons below are why: with no caches the cost depends on where the code sits in memory, so an unrelated change elsewhere in the hypervisor moves it, and the oscillator the core runs on drifts with temperature.

Expect that figure to move, and to move for reasons already known. The EL2 caches are off and the image is built -Og, which makes it an over-estimate. But ZoneX configures no clock tree yet, so the core runs on this part's power-up RC oscillator — 48 MHz, measured against the counter, and the reference manual names it the default clock for the whole system at power-up. At that clock the memory a switch touches is cheap in core cycles; raise the core clock without raising the memory's and the same code costs more, not fewer. Configuring the clock tree is a later phase, and the number is expected to change when it lands. It is published now because a measurement with its conditions stated is worth more than no measurement, and because the next one will have something to be compared against.

The image prints those conditions, the measured clock and a nanosecond conversion above its own figures on every run, along with the two things excluded from them: the guest console, because closing a partial line is a polled-UART write and a product switch has no console in it, and boundaries that had to wait out a stopped partition's window, because a switch plus a wait is neither.

examples/ holds five experiments, each a separate image: the stage-2 probe, one ThreadX guest confined by stage 2, one preempted by a timer of its own, two under a frame, and the isolation and determinism regression. Run them with scripts/test_fvp.sh on the model, or examples/s32z280_evb/tools/run_zx_probe.sh on the board.

The suite includes builds that must fail, registered as such, because a check that has never been seen to fail is not evidence that it can: a violation aimed at an address the payload is granted, an image told it needs more MPU regions than exist, a manifest whose two windows overlap, a hypervisor whose own tick cannot be delivered, a region limit one granule too generous, the per-partition time freeze removed, and a shared read-only granule its reader may write.

Along the way it settled several things about this architecture that the Cortex-R52 TRM describes ambiguously, contradicts itself about, or states somewhere nobody looks — on both a model and real silicon. The three most consequential:

  • HPRENR really is wider than 16 bits, and a bit above 15 really does disable its region — proven functionally, not by reading the register back.
  • HPFAR does not mean the same thing on the two targets. The TRM describes it two ways in one section, and the FVP and the S32Z280 each implement a different one. ZoneX uses HDFAR.
  • Setting HCR.FMO moves a guest's ICC_PMR write to the virtual CPU interface. Every guest unmasks its own interrupts at start-up; from that moment the write changes nothing about physical delivery, while the physical priority mask — which resets to zero — is left closed. A partition that was receiving its timer stops receiving anything, with no fault and no message.

docs/armv8r-el2-reference.md carries all of it, with the measured values.

Phase 0 is a partitioning demonstrator with a deliberately small scope:

  • two isolated ThreadX guest partitions on a Cortex-R52,
  • static memory partitioning through the EL2 (stage-2) MPU,
  • time partitioning driven by the hypervisor timer,
  • a stage-2 access violation decoded at EL2 and reported, then halt.

Targets are the Armv8-R AEM FVP and the NXP S32Z280-594EVB.

The region budget, and where this port will not fit

ZoneX's isolation is region descriptors in the EL2 MPU, and they are the scarce resource. Check this number against your part before you choose a board.

HMPUIR[7:0] gives the EL2 region count, and on a Cortex-R52 the architecturally permitted values are 0, 16, 20 or 24. Measured:

EL2 regions ZoneX's own MMIO the two-partition demonstrator
S32Z280-594EVB 20 2 4 of 20
Armv8-R AEM FVP 32 0 2 of 32

Three consequences, and the first decides a board.

  • A Cortex-R52 configured with no EL2 MPU cannot run ZoneX at all. Zero is a legal HMPUIR value, and there is no software fallback: on Armv8-R there are no page tables at either stage of translation, so the region MPU is the only memory control there is. A part with 16 will run ZoneX with less headroom than this bench.
  • The hypervisor's own MMIO is not free, and its cost is a property of the board rather than of ZoneX. On the S32Z280 the console and the GIC both fall in the background map's Normal write-through band, not the Device band, and a memory-mapped device reached with cacheable attributes is not a working peripheral. Each therefore costs a Device-attributed EL2 region. On the FVP both sit in the Device band and cost nothing — so the model cannot show you this constraint, and it reports 32 regions, which is not a legal Cortex-R52 value at either stage.
  • A region must also be reachable by the switch. A partition switch enables and disables region sets with one write to HPRENR, whose implemented width is a property of the part — 0x000fffff, all 20 bits, on this board. A region seated past that mask would be programmed with its own enable bit set and left there, so the outgoing partition's window would stay live under the incoming one with nothing to fault on. ZoneX checks the count and the mask separately at boot and refuses to start if the layout does not fit both.

The manifest allows four partitions of six regions each, static-asserted against 24. That ceiling is a build-time constant; the boot-time check against the real HMPUIR and HPRENR is what holds it to account on a given part.

What Phase 0 demonstrates, and what it does not

Two ThreadX kernels run at EL1 on one logical Cortex-R52 core, each confined to its own stage-2 window, time-sharing the core under a static major frame taken from a manifest. Neither can read, write or execute the other's memory or the hypervisor's — not even after granting itself that memory in its own EL1 MPU. A violation is caught by the stage-2 MPU at EL2 and reported with the partition, the address and the guest PC, and the system halts. Each partition's virtual time advances only inside its own windows.

Nothing a partition does through the schedule reaches its neighbour. Computing, masking its own interrupts and violating its boundary without pause each move the critical partition's period by tens of counts.

What reaches it is the hypervisor's own console driver. A guest that prints moves that period by up to one line tag — 22 bytes at 115,200 8N1, 17,640 counts of the board's 8 MHz counter — every run. That is a defect in ZoneX, not a limit of the partitioning, and it is bounded, derived and reproducible.

And what it does not demonstrate.

The demonstrator runs on a Cortex-R52 in lockstep, which presents as one logical core, so this is temporal and memory partitioning on a single core. It is not spatial partitioning across multiple cores; that needs split-mode SMP and is deferred.

Interrupt latency is not measured at all. Guest interrupts go straight to EL1 and cost what they always did; bounding them needs the GIC List Registers this core has and this phase does not use. Interrupt virtualisation with a bounded worst-case latency, inter-partition communication, the full time-partition scheduler, supervised partition restart, TraceX integration and the safety-artifact package are later phases and are not in Phase 0.

Every timing figure above comes from one part on one bench, with the EL2 caches off, built -Og, and with no clock tree configured. Read them as a first measurement with its conditions stated, not as characterisation.

We state this plainly because the audience for this work is safety-savvy, and because an overclaimed demonstrator is worth less than an honest one.

Architecture notes

ZoneX targets Armv8-R AArch32. Two consequences run through the whole codebase and are worth stating before you read any of it:

  • There are no page tables, at either stage of translation. Stage 2 on Armv8-R is a region-based MPU (PMSAv8-R), not a translation table. A hypervisor shaped like an A-profile one — KVM, Xen — is the wrong template.
  • The register names are the AArch32 Hyp-mode ones: HVBAR, HCR, HSCTLR, HPRBAR/HPRLAR/HPRENR/HPRSELR, HMPUIR, HSR, HDFAR/HIFAR/HPFAR, HMAIR0/HMAIR1, reached through MRC/MCR p15, 4, ….

docs/armv8r-el2-reference.md holds the verified register sheet, including the field layouts and the points that are still open. docs/decisions.md records the Phase-0 design decisions and why each was taken.

Building

ZoneX builds with CMake and Ninja. There is no dependency on any IDE.

# Host-compiled unit tests (the architecture-independent logic).
scripts/build_host.sh && scripts/test_host.sh

# Cortex-R52 cross build, Armv8-R AEM FVP example.
scripts/build_fvp.sh

ZoneX is written to C17 — it is the first Eclipse ThreadX component born on that baseline rather than migrated to it. See docs/decisions.md D12.

scripts/install.sh installs the build and test dependencies on Ubuntu.

Contributing

See CONTRIBUTING.md. Contributions are made against the dev branch, and require a signed Eclipse Contributor Agreement. AI-assisted contributions are welcome and must be attributed; that file also covers what to tell a coding agent.

Licence

MIT. See LICENSE.txt and NOTICE.

About

Eclipse ThreadX - ZoneX is a deterministic partitioning hypervisor for mixed-criticality embedded systems.

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages