Docs

Core Runtime

Understand the execution model before you wire any modules: task cadence, due checks, event routing, watchdog behavior, panic flow, and what the cooperative model can and cannot guarantee.

Why this page matters

This page explains how Core Runtime fits into the wider ZeroKernel execution model, what problem it is meant to solve, and what trade-off you are actually accepting when you use it in production firmware. The goal is not to treat Core Runtime as an isolated API call, but to understand where it sits inside bounded scheduling, queue discipline, fault visibility, and profile selection.

Read this topic as an operational contract. Start from the smallest working path, wire it into a lean profile first, and only expand into richer routing, diagnostics, or transport state after you can prove that the timing outcome is still worth the extra flash and RAM. That mindset is what keeps ZeroKernel useful on small boards instead of turning it into another bloated abstraction.

The safest pattern is always the same: define the runtime boundary, keep the hot path short, measure the effect with compare scripts, and only then scale complexity. The examples below are not filler; they show the smallest repeatable patterns you can lift into real firmware when you need clean integration instead of ad-hoc loops.

Three practical patterns

Core cadence pattern

Use one bounded task for the hot path, then let the scheduler keep the phase aligned over time.

C++
    ZeroKernel.begin(boardMillis);
ZeroKernel.addTask("Fast", fastTask, 10, 0, true);
ZeroKernel.tick();
  
Deferred work pattern

Move non-critical routing and transport out of the immediate task body so fast paths stay predictable.

C++
    const auto key = ZeroKernel.makeTopicKey("telemetry.sample");
ZeroKernel.publishDeferredFast(key, sampleValue);
ZeroKernel.flushEvents();
  
Runtime visibility pattern

Read the timing report and stats together so you can prove the cost of each abstraction layer.

C++
    const auto stats = ZeroKernel.getStats();
const auto timing = ZeroKernel.getTimingReport();
Serial.println(timing.maxTickMs);
  

What to verify while you use it

  • Validate timing before you validate aesthetics. A cleaner API is not a win if fast misses rise.
  • Prefer the smallest profile that still matches the workload, then add optional modules only when the measured payoff is obvious.
  • Keep callbacks and transport steps bounded so watchdog, panic flow, and queue limits remain meaningful.

Common mistakes that make results misleading

  • Do not copy a demo pattern into production firmware without measuring it on the real board and real build profile you plan to ship.
  • Do not read success counters without reading queue depth, timing, and workload label next to them.
  • Do not enable heavier diagnostics and compatibility flags in a lean target just because the defaults looked convenient.

Recommended working sequence

Start from the smallest valid path

Boot the runtime, register the minimum useful task set, and prove that the baseline timing is clean before adding optional layers.

Add one layer, then measure it

Introduce routing, diagnostics, or transport one layer at a time so the cost and payoff remain obvious.

Publish only repeatable results

Update docs, charts, or public claims only after the same workload survives the same validation path more than once.

What the core runtime actually owns

The core runtime is the layer that decides when work is allowed to run, how much deferred work can be drained in one scheduler turn, and how visible failure becomes when timing starts to slip. It is not a transport framework, a sensor abstraction, or a replacement for disciplined firmware design. It is the bounded execution layer that makes those higher-level pieces behave consistently.

In practice, the runtime owns five things: task cadence, queue discipline, fault visibility, kernel state, and global configuration boundaries. Once you understand those five areas, the rest of ZeroKernel becomes much easier to reason about because every feature maps back to one of them.

The most important mindset is that the core runtime is designed to stay small and predictable. It will help you structure execution, but it will not silently “fix” a bad driver, rescue a blocking socket call, or turn cooperative code into a preemptive RTOS. The payoff is determinism and clarity, not magic.

Scheduler model and due selection

ZeroKernel is cooperative and phase-aligned. A task becomes eligible when its interval says it is due, then the scheduler chooses one due task, runs it, and returns control to the main loop. If more work exists, it is handled through the next calls to tick(), not by recursively running everything at once.

Priority affects tie-breaking among tasks that are already due at the same time. It does not create preemption. That distinction matters: a high-priority task can be chosen first, but it cannot interrupt a lower-priority task that is already running. This is why keeping every callback bounded is still one of the main application responsibilities.

BehaviorWhat it means in practice
CooperativeTasks return control explicitly by finishing; nothing interrupts them mid-callback.
Phase-alignedPeriodic tasks remain attached to the intended schedule instead of drifting forever after one slow run.
Priority-awareWhen multiple tasks are due, the runtime chooses the most important one first.
Bounded drainDeferred work is consumed in controlled slices instead of becoming a hidden unbounded loop.

Runtime safety surfaces

ZeroKernel’s safety model is not a single watchdog toggle. It is a stack of runtime surfaces that work together: task execution contracts define acceptable behavior, the watchdog observes heartbeat and overrun signals, kernel state records the current escalation level, and capability gating allows the runtime to stop classes of work without deleting the tasks themselves.

This layered model matters because recovery is rarely binary. A healthy firmware should be able to pass through degraded behavior, pause non-critical modules, preserve high-priority sensing or control work, and only enter panic when the system can no longer make safe progress.

SurfaceWhat it does
WatchdogTracks heartbeat and execution windows, then escalates through degraded, safe mode, or panic flow.
Execution contractsDeclare task importance, run behavior, and escalation thresholds.
Kernel stateFormal states such as NORMAL, DEGRADED, SAFE_MODE, RECOVERY, and PANIC.
CapabilitiesGate classes of work without removing the task registration itself.
TelemetryExpose timing, queue pressure, and failure counters so compare data stays honest.

Core boot sequence examples

The safest way to think about runtime boot is to start with the smallest shape that proves the clock, then layer in task control, then add visibility. These are the three most common patterns you will actually use.

C++
    ZeroKernel.begin(boardMillis);
ZeroKernel.tick();
  

This is the absolute minimum valid runtime boot. It is useful for bring-up when you only want to prove that the runtime is alive and the board clock source is correctly wired.

C++
    ZeroKernel.begin(boardMillis);
ZeroKernel.addTask("Sample", sampleTask, 100, 0, true);
ZeroKernel.setTaskPriority("Sample", zerokernel::kPriorityHigh);
ZeroKernel.tick();
  

This is the normal first real firmware step: one task, explicit cadence, explicit priority, and a clean loop with no hidden side scheduling around it.

C++
    ZeroKernel.begin(boardMillis);
ZeroKernel.setIdleStrategy(zerokernel::kIdleYield);
ZeroKernel.setSignalHandler(onKernelSignal);
ZeroKernel.tick();
  

This pattern is for bring-up and diagnostics. It keeps the runtime visible without forcing a full debug-heavy profile when you only need signal visibility and a predictable idle behavior.

Boundaries you should respect

The cooperative model has one obvious limit: a blocking callback can still stall the loop. ZeroKernel makes the damage measurable and easier to respond to, but it does not preempt blocking work away. Treat that as a design boundary, not as a bug to wish away.

The runtime also does not replace transport discipline. If HTTP, MQTT, or WiFi work is heavy, it belongs in deferred queues or optional network modules, not in the same hot path as a 10 ms sample task. When users say a cooperative runtime “feels slow,” the root cause is usually unbounded application work, not the scheduler itself.

  • Never put a long blocking transaction directly in the fastest task on the system.
  • Prefer deferred work or network modules when a task starts mixing sensing and transport.
  • Read timing metrics and queue depth together; one number alone rarely tells the truth.

Core runtime FAQ

Does priority make this preemptive?

No. Priority only affects task selection among tasks that are already due. Once a callback is running, it still owns the CPU until it returns.

Does ZeroKernel fix blocking drivers automatically?

No. It makes the runtime more disciplined and can detect overruns, but the application still needs bounded driver patterns and sensible queue boundaries.

When should I blame the runtime instead of my task code?

Blame the runtime only after you prove that your task bodies are bounded, your transport is isolated, and your compare scripts still show a regression. Most apparent scheduler problems in embedded firmware are actually application hot-path mistakes.