Oberon RTK

ECS for Control: Schedules

Programs with several schedules, one per core: why the partition makes them straightforward, what the schedule layer adds, and the prescription for creating a schedule and moving a partition to it.

Multi-core MCUs and Multi-MCU Systems

The RP2350's and RP2040's two processor cores make it possible to split the program's responsibilities and work load. The division of work can follow different split lines, for example:

  • control laws vs monitoring and supervision
  • control laws vs mission control
  • control laws vs user interface
  • control laws for different sections of the controlled system
  • fast vs slow reacting to changes in the controlled system

Fundamental challenges include:

  • data communication, avoiding race conditions and deadlocks
  • task coordination
  • finding and moving the split line

Such divisions can also be implemented using several communicating single-core MCUs. The basic challenges remain, but are exacerbated by the need for physical links outside the MCU.

This document presents the architecture's solution and approach to programs for multi-core MCUs. The three challenges are its agenda: data communication is answered by the transports and the boundary (The Partition as the Unit, What Crosses and What Does Not), task coordination by the schedules and the kernel's rendezvous at creation (The Schedule, The Schedule Layer), and finding and moving the split line by the two parts of the prescription (Part 1, Part 2).

Multi-MCU programs are out of scope and not yet supported.[1]

The Schedule

The architecture's approach is the schedule: one run of the kernel's loop over its own set of partitions, on its own core. A program has one or more schedules. All of them are modules of one program, built into one binary, started from the part of the program running on core 0, implementing schedule 0; each schedule ticks at its own period; where two schedules exchange data, the program declares it once, in the boundary, and the kernel checks the schedules' plans against each other. A program with one schedule is the same program with one schedule module and nothing crossing.

What makes this a small step rather than a second architecture is the partition.

The Partition as the Unit

A partition is designed and implemented for one schedule, but is fully agnostic regarding which schedule will run it. Five of its properties, each held for its own reason on one core, are what carry it to a second one.

The store is private, and two things are reachable from outside. A partition's Component store, its variable and its locked part, is imported by nothing outside the partition. What a peer partition can reach is exactly two things:

  • the surfaces the partition's token module publishes. A surface is a transport the partition hosts, under one name: the set of tokens a peer's access to that transport reaches – the transport itself, and any observation its adapter records beside it, such as the clip tally beside the print buffer: the adapter is a route, so what it writes on the peer's access is the peer's write, and the peer's manifest must cover it. A peer declares the surface in its manifest, wholesale; it never names the partition's tokens one by one, and it need not know them;

  • the interface record of its locked part, values locked at load, such as a writer handle or a calibration.

Nothing else is visible, so nothing else can depend on where the partition runs. Every surface of the evaluation programs is a transport's surface, and the next property says what a transport is.

The constructs: chapters Components State, Components Locked and Tokens.

Partitions meet only through transports, and every transport is a two-party channel. Data crosses a partition boundary only by being published into a transport hosted in the producing partition's store – the transport behind a surface: a sampling port for the latest value, a queueing port for messages in order, a ring buffer for a stream.

Each has, by construction, one producer and one consumer: the sampling port is Simpson's four-slot algorithm, one writer and one reader, wait-free; the ring buffer and the queueing port are single-producer single-consumer rings. A transport is therefore not a shared object that two parties contend for; it is a channel with a fixed end on each side, and it is lock-free because each end is owned by one party – on one schedule as on two. What the second core adds is only the ordering of memory: on one core the program order already orders the publication, on two the primitives' memory barriers do.

A partition boundary is thereby a schedule boundary in waiting: the mechanism that separates two partitions on one core is the same mechanism that separates them on two.

The constructs: chapter Ports and Adapters, sections The Protocols and Per-Seam Port Modules.

A System reaches a transport through an adapter, and the adapter's procedure types carry the direction. The hosting partition exports an adapter, one procedure per direction the seam is used in, and beside each procedure its procedure type. A System binds the adapter's procedure into its runSystem signature and reads or writes a value through it; it imports nothing of the hosting partition's store and knows no store but its own. Whether the producer behind the adapter runs on this core or on the other is not visible in the System, in its signature, or in its manifest.

The constructs: chapter Ports and Adapters, section Adapters; chapter System Modules.

A manifest is a deployment-independent declaration. Each System declares what it reads and writes as sets of tokens; it does not say where the producer runs. The declaration is the same with one schedule and more, and the kernel checks it the same way, against the other Systems of its schedule and, through the boundary, against the other schedules' plans.

The constructs: chapter System Modules, section The Manifest.

One token space names the store variables program-wide. Tokens are allocated once for the whole program, each partition's range from one base, so that a token means the same store variable on every core. That is what lets a boundary name what two schedules share, and what lets two schedules' plans be checked against each other in one vocabulary.

The constructs: chapter Tokens.

The partitions are composed from outside. A partition exports two procedures, Create and AddSystems; the schedule module calls them, in its station sequence, and the kernel registers the Systems into the context of the core that calls. A partition's placement is two lines in a schedule module, not a property of the partition.

The constructs: chapter The Main Program and the Partitions.

Under these properties a schedule is a property of the program around a partition. A program with several schedules differs from a program with one in its program-level modules only – the schedule modules, the boundary, the periods – and a partition is a unit that can be moved between schedules, and between programs of one family, as it stands.

The Schedule Layer

The program level composes the partitions: the modules that place them on schedules, declare what crosses between schedules, and run them.

The picture is one of layers and one vertical cut:

  • at the bottom the hardware, asynchronous where it is, met by the partitions through their bindings;

  • the kernel, which drives the Systems of each schedule in its loop and synchronises the schedules at creation only;

  • the partitions, whose Systems run synchronously within their schedule;

  • and the program level, which composes them.

The boundary between schedules is not a layer: it is the vertical cut between schedules within the partition layer, and what crosses it is data.

These are the same modules in a program with one schedule and in a program with several; the constructs are shown in the implementation document, chapters Schedules and the Boundary and The Kernel.

  • The schedule modules. Each schedule has one: the module that runs the station sequence for the schedule's partitions – installation, creation, registration, boundary, plan, report, commit, run. Schedule 0's module is the program's main module; it alone runs the consistency station, first, and then starts the other cores: a program whose definitions disagree has no business starting another core. Every other schedule's module exports its sequence as Run, started on its core by schedule 0, and its first station configures that core's infrastructure.

  • The boundary module. The crossings between schedules, declared once for the program: two matrices of token sets indexed from schedule to schedule, one for dataflow and one for deposits, each entry a union of surfaces. Every schedule module passes the same record to the kernel. The kernel derives from it the schedule's view of the others – what arrives from elsewhere counts as produced, what leaves counts as consumed – and checks the schedule's plan against it. A program in which nothing crosses has no boundary module.

  • The kernel. It keeps a context per schedule, selected by the core that calls, so that every procedure of its interface acts for the calling schedule; one token space serves all schedules. Twice during creation the schedules wait for each other: after each has planned, so that the cross checks – no token written on two schedules, every declared counterpart present – see every plan; and after each has committed, so that no schedule runs before every schedule may. With one schedule both waits return at once.

  • Time. The kernel's timer is one and shared: schedule 0 binds it, every schedule reads it, so that timestamps and pass times are comparable across cores. The tick is per core, with its own period from the periods module, TickPeriodS0, TickPeriodS1; a System's period is counted in the ticks of the schedule it runs on.

  • The error console. One terminal for the program's errors and diagnostics, shared by the cores, with a hardware spinlock behind it: the run-time error path of either core claims it before printing, and a schedule's own error messages and diagnostics claim and release around what must not interleave.

  • The memory map. It declares the number of cores, which sizes the kernel's contexts and its rendezvous, and it allocates each core's stack, heap and vector table. The framework's MemMap for the RP2350 serves both cores; a program with one schedule carries a map declaring one.

What Crosses and What Does Not

Three rules follow from the properties above, and the second part of this document applies them step by step.

  • Data crosses only through transports, named by surfaces. A crossing in the boundary names a surface, never a token: what a partition publishes is what may cross, and a schedule boundary is always a partition boundary. If a token that should cross is a plain store variable, the design needs a transport first.

  • All depositors of a facility run on one schedule. The deliberately multi-writer set of the manifest, the deposit, is sound because the schedule is the mutual exclusion among the depositors. Depositors on two schedules would be two concurrent writers of one transport. A facility with participants on both schedules is two facilities; a collector on the other schedule is a deposit crossing, declared in the boundary's deposit matrix.

  • No lock, anywhere. The architecture excludes locks on the program's Components altogether. Within a schedule the rule costs nothing: the schedule is the mutual exclusion, and there is nothing to lock. Across schedules a lock is what other architectures reach for, and the rule holds there too: the kernel synchronises the schedules at creation only, and across the boundary the only coordination is a transport's protocol – one producer, one consumer, data published and read. A program may build a wait on that, a System on one schedule doing nothing until a value from the other schedule changes: that is a protocol over data, within run-to-completion, not a lock. A facility coordinated across schedules would need a lock and is therefore not built. The one lock in the program, the lock of the error console, guards a terminal on the error path, not Components.

The Program with One Schedule

A program with one schedule is the same program: one schedule module, which is its main module; no boundary, since nothing crosses; one tick period; its own memory map declaring one core. The kernel keeps one context, its two rendezvous return at once, no cross check can fire, and the plan report needs no brace since one core prints. Nothing in the partitions is different, which is the point: a program is written for one schedule and gains a second by the steps below.

Aim and Scope of the Prescription

Both parts below are about several schedules on one MCU with several cores: one program, one build, one binary, one token space, one ECS file with one PROGRAM group. The schedule modules are modules of that program, started from schedule 0's module; they share the store's memory, in which the boundary's transports live, and the one error console.

Both parts rest on the fact the first half established: a partition is schedule-agnostic by construction. Nothing inside a partition directory knows which schedule runs it. Creating a schedule or moving a partition touches the program-level modules only – the schedule modules, the boundary, the periods – and the ECS file, with one exception outside the operational path: diagnostic output to the error console from within a System (1.10). If any other step seems to require a change inside a partition directory, the design has a gap: stop there and record it. Both parts were executed on the evaluation programs: Part 1 is how EcsControlSchedule-v29 came from -v26, Part 2 how -v30 came from -v29.

Part 1: Creating a Schedule

Starting point: a program with one schedule – one main module whose station sequence creates every partition, as EcsControlSchedule-v26. Goal: a second schedule with its first partition, as EcsControlSchedule-v29: schedule 1 created, and the adaptation partition moved onto it. A schedule is created for a partition, so the steps of creating one include the steps of a move – the preflight, the boundary, the periods, the two schedule modules – which Part 2 repeats for a further partition. The steps use n = 1 throughout; a third schedule repeats them with n = 2.

1.1 Peripheral Reach

Before anything else: every peripheral the new schedule's partitions will bind must be reachable from the new core. On the RP2350 both cores reach every peripheral alike, except the ones on the private peripheral bus – the SysTick, the interrupt controller, the fault registers – which are per core and which each schedule module configures for its own core. Other MCUs give a core private peripherals or a subsystem its own; a port of the framework verifies this per MCU. Secure and Non-secure attribution is a separate axis, untouched here.

1.2 The Memory Map: Cores and Memory

Module MemMap does two things:

  • NumCoresUsed defines the number of cores used for a program, which sizes the kernel's internal context data structures, as well as the number of slots of its two rendezvous. It must equal the number of schedules. A two-core value for a one-schedule program will cause the one schedule to wait forever at the rendezvous.

  • The map allocates each core's memory for data, heap, stack, and vector table, based on the configuration values for Astrobe (.ini config file), accessed via module LinkOptions. For a one-schedule program, all memory is usually allocated to core 0, for a two-schedule program the available memory can be split flexibly as needed; a symmetric allocation is usually a good starting point, as the framework's MemMap does.

A program-local module MemMap will automatically be selected and used via module Main. In absence of a program-local map, the framework's default will be picked. A program-local map is a program module and declares its kind, (* ECSKIND: OTHER *): a module the definition places and says nothing further about.

The step is therefore: switch the program from its one-core MemMap to a two-core one, either by editing the program-local copy, or by removing it (source, symbol and object files) to import the framework's default. Editing means to set NumCoresUsed correctly, as well as adding the memory allocation declarations for core 1. Since Astrobe only supports the memory allocation definitions for core 0, the corresponding configuration data need to be adjusted as needed to make space for core 1 memory. Missing or erroneous memory allocations will result in unpredictable faults, most likely at Cores.StartCoreOne, and unreported, since no error reporting is installed on core 1 at that point.

1.3 The Periods Module: the Schedule's Tick

Add, beside the existing tick period:

    TickPeriodS1* = 50;   (* milliseconds *)

If the program still calls its tick TickPeriod, rename it TickPeriodS0 and every derived period with it (... DIV TickPeriodS0). One tick constant per schedule, named by the schedule number; in EcsControlSchedule-v29 both are 50 ms.

1.4 Preflight: Which Surfaces Cross

Only surfaces cross a partition boundary, and a schedule boundary is a partition boundary; so the question is not which tokens of which System cross, but which surfaces do. Two lists answer it, both read off the token modules and the manifests, not off the code:

  • The surfaces the moving partition publishes – its token module's I record – that a partition on the other schedule consumes. For the adaptation partition: Tadapt.I.drainCmd, consumed by the print partition's drain, which stays on schedule 0: a crossing from schedule 1 to schedule 0.

  • The foreign surfaces the moving partition's Systems declare – the Tp.I.x references in their manifests – whose publisher stays on the other schedule. For the adaptation partition: Tprint.I.clipsObs, published by the print partition: a crossing from schedule 0 to schedule 1.

A surface is a transport's surface by construction (The Partition as the Unit), so there is nothing further to verify per crossing. Only a discrepancy sends you to the Systems: a manifest that names a foreign token outside a surface, Tp.L.x, is a violation on its own, and a System that reaches foreign data by any path but an adapter is another; both are findings of the review, not of the move.

1.5 The Boundary Module

The program declares the crossings once, in a new program-level module B, declared BOUNDARY with the crossings variable as its role: the record, a CLEAR, the assignments, no procedures. EcsControlSchedule-v29:

(* ECSKIND: BOUNDARY Crossings *)
MODULE B;

  IMPORT Kernel, Tprint, Tadapt;

  CONST
    S0 = 0; S1 = 1; (* schedules *)

  VAR
    Crossings*: Kernel.Crossings;

BEGIN
  CLEAR(Crossings);
  (* from -> to *)
  Crossings.flow[S0, S1] := Tprint.I.clipsObs;
  Crossings.flow[S1, S0] := Tadapt.I.drainCmd
END B.

Two rules:

  • A matrix entry is a set: crossings in the same direction are unioned with +.

  • Every term is a surface, Tx.I.s, never a braced token. A token another partition consumes is published through the partition's surface; a schedule boundary is always a partition boundary; so a partition's crossings are exactly its surfaces' sets. A braced token in the boundary would mean a cross-partition reference bypassing the surface, which is a violation on its own, or a partition split across schedules, which the architecture excludes.

1.6 The Periods Module: the Moved Systems' Periods

Every period of the moved partition's Systems that is derived from the tick is re-derived from the new schedule's tick, since periods are counted in the ticks of the schedule the System runs on:

    (* adapt partition *)
    PrintAdaptPeriod* = 600 DIV TickPeriodS1;

where it was DIV TickPeriodS0. EachTickPeriod needs no change: it means every tick of whatever schedule. This is the one step nothing checks: a period left on the old basis compiles, plans and runs – at the wrong rate. Periods are not the only quantities counted in ticks; 2.6 lists the others and which schedule's tick each belongs to.

1.7 The New Schedule Module

A new file at program level, named for its schedule and declared SCHEDULE with Run as its gate, with the station sequence in the exported Run and the moved partition's two calls in it:

(* ECSKIND: SCHEDULE Run *)
MODULE EcsControlScheduleS1v29;

  IMPORT
    Kernel, R, P, B, N, Main, ErrConsole, PlanView, AdaptPartition;

  PROCEDURE Run*;
    CONST Schedule1 = 1;
  BEGIN
    Main.ConfigC1;
    Kernel.Install(P.TickPeriodS1, R.ExcPrio_Systick, R.TimerHandle_Kernel, R.TimerUnit_Kernel);
    AdaptPartition.Create;
    Kernel.Begin;
    Kernel.SetBoundary(B.Crossings);
    AdaptPartition.AddSystems;
    Kernel.Plan;
    ErrConsole.Claim;
    PlanView.Report(ErrConsole.Werr, N.Names, Schedule1);
    ErrConsole.Release;
    Kernel.Commit;
    Kernel.Run
  END Run;

END EcsControlScheduleS1v29.

Four points:

  • No module body statement: the sequence is the exported Run, started by schedule 0's module (next step). The first station, Main.ConfigC1, configures the new core's infrastructure, its run-time error reporting included.

  • No consistency station (module C): the agreements between partitions are asserted once, on schedule 0.

  • The Install line passes the same timer handle and unit as schedule 0's Install: core 0 binds the kernel's timer, the new core's Install asserts it bound, and both read the same clock.

  • The partition's Create and AddSystems are the two calls that leave schedule 0's module in the next step; the boundary station stands between them, after Kernel.Begin.

1.8 Schedule 0's Module

In the main module, schedule 0's module, three changes. Import Cores and the new schedule module, and add the start as the station after the consistency station and before the installation; if the definitions disagree, there is no point in starting another core:

    C.Assert;
    Cores.StartCoreOne(Core1Prog.Run, NIL);
    Kernel.Install(P.TickPeriodS0, R.ExcPrio_Systick, R.TimerHandle_Kernel, R.TimerUnit_Kernel);

with Core1Prog := EcsControlScheduleS1v29 in the import list. Remove the moved partition's import and its two calls, AdaptPartition.Create and AdaptPartition.AddSystems. Add the boundary station after Kernel.Begin, Kernel.SetBoundary(B.Crossings), with B imported. Rename the main module for its schedule if it is not yet so named – EcsControlScheduleV26 became EcsControlScheduleS0v29; the program's label in the ECS file is not a module's name, so no collision arises.

The moved partition's directory – its Systems, adapters, ports, store, token, wiring and values modules – is untouched.

1.9 Devices and Pins

A device – a UART, pins, a timer – belongs to the partition that binds it: its handle sits in that partition's locked part, its Create constructs and configures it, and its Systems alone drive it. The device moves with the partition, and nothing changes; the only question is whether the new core reaches it (1.1). A device used from two partitions would be a meeting place beside the store, which the architecture excludes: if a move seems to need a change here, the finding concerns the design before the move. A handle a partition publishes in its locked interface, such as the print partition's writer handle, is not a shared device but the access path to a transport that partition hosts; the device behind the transport stays with its partition. The adaptation partition binds nothing.

1.10 The Error Console

The error console is one per program, and the new core writes to it like the first; Main.ConfigC1 plugs the console's claim and reset into the new core's run-time error reporting, so that an error report from either core claims the console before printing. Consequence: error messages and diagnostics the new schedule prints go between ErrConsole.Claim and ErrConsole.Release, as the plan report does. Any such output from schedule 0 needs to be braced accordingly as well: a System that printed unbraced in the one-schedule program gets its braces, the one change a second schedule can require inside a partition. It concerns diagnostics only; output that belongs to the program's operation goes through the print facility and needs no brace. In the evaluation programs every printing System sits on schedule 0, so none was changed.

1.11 Build

The new schedule module is in schedule 0's import closure, so the compile reaches it, and it sits at program level, so the project's search path needs no new entry. Compile and build; the binary is named after schedule 0's module.

1.12 The ECS File

If the program keeps an ECS file: the PROGRAM group gains the second SCHEDULE entry and the BOUNDARY entry; a SCHEDULE group lists the moved partition under the new schedule module, and the partition leaves schedule 0's group; in the DECLARATIONS, the new schedule's entry carries its deployment call as the source writes it, deployment = Cores.StartCoreOne(Core1Prog.Run, NIL), and the boundary's entry its two crossings. extract-ecs regenerates the declarations; check-ecs then verifies, among its check-points, that the schedules are dense and each started once, that schedule 0's gate is local and body-called and the other's exported, that each crossing's producers sit in its from schedule and its consumers in its to schedule, and each schedule's plan against the boundary. The station sequence is read by the reviewer, not by the tools.

1.13 Run

On the console: schedule 0's plan report – eight Systems, and a schedule crossings section listing the observation port as produced here and consumed there, the command port the other way round – then schedule 1's report, with AdaptDwell alone and the mirror image of the crossings.

Both schedules start on tick 1, since the kernel's second rendezvous precedes the tick enable on every core. The program behaves as before: the adaptation loop now runs on core 1, its observations arriving and its commands leaving across the boundary through the two sampling ports.

Part 2: Moving a Further Partition

The worked case is EcsControlSchedule-v30: the coordination partition, CoordPartition, moves from schedule 0 to schedule 1, which already runs the adaptation partition. In the steps, a is the schedule the partition leaves, b the one it joins, X the partition. The steps are those of Part 1 that concern the partition, with the schedule and the boundary already in place.

2.1 Preflight: Which Surfaces Cross

As in 1.4, two lists. For the coordination partition:

  • Published and consumed on the other schedule: Tcoord.I.readPeriods, consumed by the blink partition's BlinkSystem, which stays on schedule 0: a new crossing, from b to a.

  • Foreign surfaces its Systems declare: none; PeriodSystem consumes nothing from other partitions.

Crossings X already had with schedule b would become internal and leave the boundary: none here, since the coordination and the adaptation partition do not meet.

2.2 The Boundary Module: Add the Crossing

The new crossing joins the entry of its direction, unioned with +.

EcsControlSchedule-v30:

  IMPORT Kernel, Tprint, Tadapt, Tcoord;
  ...
  Crossings.flow[S0, S1] := Tprint.I.clipsObs;
  Crossings.flow[S1, S0] := Tadapt.I.drainCmd + Tcoord.I.readPeriods

The rules of 1.5 hold: one assignment per entry, surfaces only.

2.3 The Periods Module: the Moved Systems' Periods

As in 1.6, for the moved partition's Systems:

    (* coord partition *)
    PeriodPeriod* = 5000 DIV TickPeriodS1;

where it was DIV TickPeriodS0.

2.4 The Schedule Modules

Schedule a's module: remove the import of X and its two stations:

    CoordPartition.Create;          (* remove *)
    CoordPartition.AddSystems;      (* remove *)

Schedule b's module: add X to the import list, and the two stations in their groups:

    AdaptPartition.Create;
    CoordPartition.Create;
    Kernel.Begin;
    Kernel.SetBoundary(B.Crossings);
    AdaptPartition.AddSystems;
    CoordPartition.AddSystems;

The order within a group is irrelevant: no partition's Create reads a peer's store, so the creations compose in any order, and the order of registration is meaningless by design. Both schedule modules already have the boundary station.

The moved partition's directory is untouched.

2.5 Devices and Pins

As in 1.9: the devices X binds move with it, and the only check is the target core's reach. The coordination partition binds nothing; the blink partition's LEDs stay with it on schedule 0.

2.6 Ticks as a Unit: Self-Timed Systems and Values That Cross

A tick is a unit local to its schedule: schedule 0's tick and schedule 1's are different units once their periods differ. Every quantity counted in ticks therefore has a schedule it belongs to, and a move must leave each quantity in the unit of the schedule that counts it. Four cases:

  • A System's registration period in the periods module, 2.3: counted by the kernel in the ticks of the schedule the System runs on; re-derived from the new tick.

  • A self-timed System's reload values. DrainSystem keeps its own tickers in the store, decrements them by Kernel.ElapsedTicks() each pass, and reloads from the periods its locked part holds, CL.L.bufDrain.cfgs[i].periods[cmd], authored in Vprint as 600 DIV P.TickPeriodS0 and the like. These are counted by the drain's own schedule, so they are denominated in its tick: if the print partition moved, every tick-denominated value of its values module would change its divisor with it, exactly as the periods module does. The rule of 2.3 extends to the moved partition's values module.

  • A tick-denominated value that crosses the boundary. BlinkSystem self-times in the same way, decrementing by its schedule's ElapsedTicks() and reloading from the period value it reads through the coordination partition's port. That value is authored in Vcoord, in the coordination partition, which in EcsControlSchedule-v30 runs on schedule 1 – and it is still written 300 DIV P.TickPeriodS0, because the unit is the consumer's tick: BlinkSystem counts it on schedule 0. A tick-denominated value that crosses a boundary is denominated in the tick of the schedule that counts it, which is not the producer's tick. The move of the coordination partition therefore left Vcoord unchanged, and a reader of Vcoord must know why its divisor names another schedule's tick. The cleaner design, where a seam carries such values, is to cross in a unit that is not schedule-local – milliseconds – and let the consumer convert against its own tick; the evaluation programs keep the tick-denominated form and the comment.

  • Phase references. DrainSystem re-locks its tickers to the tick grid, ticks[i] := delta - ((Kernel.TickCount() - 1) MOD delta): the tick count as a phase reference on its own schedule's grid, which is the legitimate use of the count. It stays correct through a move because TickCount() answers for the schedule the System runs on. A tick count used as a clock, as if it measured time, is the misuse 2.10 exposes.

Kernel.TickTime() stands apart from all four: it is not a count of ticks but a time, read from the one shared timer at the start of the pass, in microseconds. Every System of a schedule sees the same value within one pass; two schedules see values from the same clock, comparable with each other, but not the same value, since their passes start at different moments and their ticks drift against each other. A quantity that must mean the same on both schedules is expressed in that time, not in ticks.

2.7 Unchanged by Construction

The consistency module, the token allocation, the token names, the resources, every wiring and values module, every partition file, the framework's start-up, the library, and the project's search path: the partition directory stays where it is.

2.8 The ECS File

If the program keeps an ECS file: in the SCHEDULE groups, the X: PARTITION entry moves from schedule a's group to schedule b's group; the boundary's declaration gains the new crossing. extract-ecs regenerates the declarations; check-ecs verifies as in 1.12.

2.9 Run

On the console: schedule a's plan report, its schedule crossings section listing the period port among the others; then schedule b's report, with the moved Systems in its order and the crossings' mirror image. Both schedules start on tick 1. The program behaves as before the move: the LEDs blink as in -v29, their period steps now arriving across the boundary through the sampling port.

2.10 The Rate Test

Change schedule b's tick period and nothing else – TickPeriodS1* = 50 to 10 in EcsControlSchedule-v30. Every period of schedule b re-derives itself to the same milliseconds. Expected: identical behaviour at the plant and in the diagnostics; schedule b's plan report unchanged; no missed ticks on either schedule. Before the test, check that every period derived from the new tick still yields the intended milliseconds: ticks times tick period, before and after. If the behaviour changes with the tick rate, a quantity of schedule b is counted in the wrong schedule's tick (2.6): a reload value whose divisor did not follow the move, a value crossing the boundary in the producer's tick, or a tick count used as a clock.

What the Two Parts Show

Creating a schedule with its first partition is one new schedule module, one new boundary module, and edits in two files, the periods and schedule 0's module. Moving a further partition is four program-level files – the boundary, the periods, the two schedule modules. Neither touches a partition file. The partition is the unit, and its schedule is a property of the program around it. The same property makes a partition portable across programs of one family: the print partition of the evaluation programs is one set of files in all three, the one difference between them being the tick period its drain divides by.

See Also


  1. In particular the usage of ports for inter-core communication should facilitate the extension of the architecture for multi-MCU solutions, but only a running implementation will provide a sound conceptual and technical basis. One of the conceptual challenges is a coherent token space across several binaries. ↩︎

Last updated: 2 November 2026