Small Bits. Solid Systems.
ZeroKernel is the embedded execution runtime built by ZeroBits for microcontrollers that need deterministic scheduling, bounded memory, and fault-aware orchestration without the weight of a full RTOS.
Official URL: https://kernel.zerobits.tech
ZeroKernel is designed for firmware that has outgrown ad-hoc loop() code but does not need a preemptive RTOS.
- Deterministic cooperative scheduling
- Fixed-capacity runtime with no dynamic allocation in the active path
- Key-based event and command routing for lean builds
- Watchdog, safe mode, panic routing, execution contracts, and capability masks
- Build profiles that scale from small always-on nodes to diagnostics-heavy bring-up
The intended position is simple:
- stronger than a basic scheduler library
- smaller and easier to reason about than a full RTOS
- practical for production firmware on ESP8266, ESP32, RP2040, STM32, and similar targets
- Cooperative interval scheduler with priority-aware task selection
- Fixed-size task registry and bounded queues
- Pub/sub events, command queue, work queue, and cooperative event flags
- Watchdog supervision with heartbeat timeout, overrun handling, recovery, and safe mode
- Runtime identity, ABI version, manifest, timing reports, diagnostics hooks, and task-scoped capability gating
- Low-level internal helpers for cycle counting and idle hints, including C and assembly where it actually helps
The repository now ships with project-level release and validation scaffolding:
- GitHub Actions CI on every push to
mainand on pull requests - Tag-driven release packaging on
v*tags - A local
CHANGELOG.mdfor release notes and regression history - Wiki-ready documentation pages under
docs/wiki/
Start points:
docs/wiki/Home.mddocs/wiki/Validation.mddocs/wiki/Beta-Modules.md
Latest measured references:
- ESP8266
POWER_SAVE(UniversalSmokeTest):28864 / 80192RAM - Wemos compare runtime overhead vs blocking baseline:
+744 bytesRAM - Wemos determinism gate:
fast_avg_lag_us=0,fast_max_lag_us=0,fast_miss=0 - ESP32 compare runtime overhead vs blocking baseline:
+704 bytesRAM - ESP32 determinism gate:
fast_avg_lag_us=0,fast_max_lag_us=0,fast_miss=0 - Wemos measured free heap during diagnostics:
49280
These are regression references, not marketing numbers. The most important gate is still deterministic timing.
Measured on Wemos D1 mini using the blocking baseline and the current ZeroKernel compare sketch:
| Metric | Before (blocking) | After (ZeroKernel) |
|---|---|---|
| RAM usage | 28300 / 80192 |
29044 / 80192 |
| Fast avg lag | 2512 us |
0 us |
| Fast max lag | 11054 us |
0 us |
| Fast misses | 126 |
0 |
Tradeoff summary:
- RAM overhead:
+744 bytes - Determinism maintained:
0 lag,0 misses - Measured free heap on Wemos diagnostics:
49280
That overhead buys bounded scheduling, watchdog supervision, panic flow, capability-aware task gating, and fixed-capacity queues. On ESP8266-class hardware the measured free heap remains comfortably high, so the extra runtime footprint is a small and controlled tradeoff rather than a practical memory risk.
Measured on ESP32 using the blocking baseline and the current ZeroKernel compare sketch:
| Metric | Before (blocking) | After (ZeroKernel) |
|---|---|---|
| RAM usage | 22116 / 327680 |
22820 / 327680 |
| Fast avg lag | 2022 us |
0 us |
| Fast max lag | 8156 us |
0 us |
| Fast misses | 124 |
0 |
Tradeoff summary:
- RAM overhead:
+704 bytes - Determinism maintained:
0 lag,0 misses
The ESP32 tradeoff is also healthy: the footprint increase is small relative to total headroom, while the scheduling result is materially better under the same workload.
The optional network helpers are currently marked BETA:
ZeroTransportMetricsZeroHttpPumpZeroMqttPumpZeroWiFiMaintainerZeroNetProfileEsp8266(recommended constrained starting point for Wemos / ESP8266)ZeroNetProfileEsp8266Http(opt-in constrained HTTP-first variant for Wemos / ESP8266)
They are already useful and validated on desktop plus ESP32 hardware, but they are still under active tuning for footprint, retry behavior, and cross-board transport quirks. The core runtime is the stable layer; the network helpers should be treated as add-on modules that are ready for evaluation and controlled deployments.
Current target maturity:
- ESP32: stable enough for production-style evaluation and controlled deployments when validated against the real HTTP/MQTT endpoint you intend to ship with.
- ESP8266 / Wemos: still BETA for full dual-transport use. The constrained MQTT-first preset is now the recommended path and already has repeatable live runs where MQTT delivery is real and timing beats the naive baseline, but full HTTP+MQTT behavior is still under active hardening.
- Other supported families: compile-validated, but network helper maturity should still be treated as evaluation-grade until they receive the same live validation depth.
Recommended board-specific path:
- ESP32: use the default network module configs first, then validate against your real endpoint.
- ESP8266 / Wemos: start with
ZeroNetProfileEsp8266. It is a constrained MQTT-first preset that disables periodic HTTP dispatch by default, prevents offline queue buildup, lowers idle MQTT churn, staggers lighter network task cadence, and recommends a lighter idle strategy. In current validation it is the preferred path for Wemos MQTT delivery, while HTTP remains degraded/off by default unless you deliberately opt back in. - ESP8266 / Wemos (HTTP-specific): use
ZeroNetProfileEsp8266Httponly when your node is intentionally HTTP-first. It keeps the same constrained timing discipline, but does not try to combine HTTP and MQTT in the same default path. Current validation now shows real HTTP delivery with clean request success under the official local compare, but it is still marked experimental because misses and worst-case lag are not yet as tidy as the MQTT-first constrained path.
Current best module tradeoff reference (ESP32, LEAN_NET, manual pattern vs module pattern):
- RAM overhead:
+408 bytes - Determinism maintained:
fast_miss=0 - HTTP success count:
6 -> 9 - MQTT success count:
10 -> 18 - Queue pressure:
4 -> 1
In other words: the modules do cost memory, but the current tuned path keeps that cost bounded and pays it back with better transport throughput and less queue buildup under the same synthetic workload window.
The current network helper stack is now strong enough on ESP32 to treat as a practical deployment target, not just a lab demo, as long as you still validate it against your own endpoint, payload size, and retry policy.
Current accepted live compare reference on ESP32:
- baseline:
sample_runs=47fast_avg_lag_us=112955fast_max_lag_us=1609577fast_miss=12http_rate=73mqtt_rate=73
- modules:
sample_runs=85fast_avg_lag_us=15857fast_max_lag_us=1047913fast_miss=5http_rate=66mqtt_rate=100
The important part is the shape of the tradeoff: timing improves materially, MQTT delivery becomes clean, and HTTP stays alive under the same live window. That is why the ESP32 path is now documented as stable enough, even though the global module label remains BETA until the ESP8266 path is cleaned up too.
To avoid overfitting the runtime to a single project, ZeroKernel now also ships with three reusable compare workloads:
EnvMonitor: sensor sampling + filtering + threshold alarmTelemetryGateway: queue-heavy transport orchestration without tying the example to real Wi-Fi credentialsIndustrialLoop: fast control loop + command handling + diagnostics + safe-mode recovery
Current ESP32 references from the compare runners:
EnvMonitorsample_runs:49 -> 50fast_avg_lag_us:871 -> 0fast_max_lag_us:1780 -> 0fast_miss:24 -> 0
TelemetryGatewayhttp_ok:23 -> 27mqtt_ok:23 -> 27http_rate:88 -> 87mqtt_rate:92 -> 90http_queue:0 -> 0mqtt_queue:0 -> 0
IndustrialLoopcontrol_runs:99 -> 101fast_avg_lag_us:241 -> 0fast_max_lag_us:3983 -> 0fast_miss:9 -> 0recoveries:0 -> 1
These workloads are intentionally broader than the micro-benchmarks: they show the runtime under sensor, transport, and control-loop pressure without depending on a single application codebase.
Measured on a real ESP8266 direct-AP seismic node using:
- NodeMCU / ESP8266
- MPU6050-class accelerometer over I2C
- local buzzer alarm output
- direct access point mode with queued HTTP delivery
- live local backend over Wi-Fi AP during the test window
The original firmware was a single blocking loop. The ZeroKernel rewrite split the workload into sampling, heartbeat, flush, buzzer, temperature, and status tasks.
| Metric | Before (direct loop) | After (ZeroKernel, tuned) |
|---|---|---|
| Sample runs (5s window) | 476 |
501 |
| Fast avg lag | 5393 us |
6 us |
| Fast max lag | 21733 us |
2378 us |
| Fast misses | 406 |
1 |
| Successful local sends | 5 |
7 |
Tradeoff summary:
- The direct-loop version remains functional, but it drifts and drops timing quality once live HTTP delivery is active.
- The tuned ZeroKernel version keeps the same node online, preserves successful local delivery, and holds sensor timing far closer to the target schedule.
- This is the kind of workload where the difference is visible on real hardware under real transport load, not only in a synthetic lab loop.
Real sample window from the direct AP seismic project:
BASELINE_SEISMIC window_ms=5009 sample_runs=476 fast_avg_lag_us=5393 fast_max_lag_us=21733 fast_miss=406 queue_max=1 sent_ok=5 sent_fail=0 captures=5 clients=1
ZEROKERNEL_SEISMIC window_ms=5000 sample_runs=501 fast_avg_lag_us=6 fast_max_lag_us=2378 fast_miss=1 queue_max=3 sent_ok=7 sent_fail=0 captures=7 clients=1
Representative baseline vs ZeroKernel shape from the seismic firmware:
Baseline-style direct loop:
void loop() {
const unsigned long nowUs = micros();
trackSampleTiming(nowUs);
++sampleRuns;
float ax = 0.0f;
float ay = 0.0f;
float az = 1.0f;
readAccel(ax, ay, az);
updateMotionModel(ax, ay, az);
if (shouldSend && canCapture) {
capturePacket(true);
} else if (heartbeatDue() && hasDirectClient()) {
capturePacket(false);
}
if (txCount > 0) {
flushQueueOnce();
}
delay(LOOP_DELAY_MS);
}ZeroKernel rewrite:
void sampleSensorTask() {
trackSampleTiming(micros());
++sampleRuns;
float ax = 0.0f;
float ay = 0.0f;
float az = 1.0f;
readAccel(ax, ay, az);
updateMotionModel(ax, ay, az);
if (shouldSend && canCapture) {
capturePacket(true);
}
}
void setup() {
ZeroKernel.begin(zerokernel::adapters::arduinoMillisClock);
ZeroKernel.addTask("Sample", sampleSensorTask, LOOP_DELAY_MS, 4);
ZeroKernel.addTask("Flush", queueFlushTask, 120, 3);
ZeroKernel.addTask("Heartbeat", heartbeatTask, 25, 2);
ZeroKernel.addTask(buzzerTaskConfig);
ZeroKernel.addTask(tempTask);
ZeroKernel.addTask(statusTask);
ZeroKernel.setTaskPriority("Sample", zerokernel::Kernel::kPriorityCritical);
ZeroKernel.setTaskPriority("Flush", zerokernel::Kernel::kPriorityLow);
}
void loop() {
ZeroKernel.tick();
}That rewrite is the practical difference: the same node behavior is split into bounded, phase-aligned tasks instead of one blocking loop that mixes sensing, transport, alarms, and status work together.
The richer ESP32 telemetry workload is also now phase-aligned after the runtime scheduling fix:
- Baseline:
sample_runs=21,fast_avg_lag_us=0,fast_max_lag_us=0,fast_miss=0 - ZeroKernel:
sample_runs=21,fast_avg_lag_us=0,fast_max_lag_us=0,fast_miss=0
This matters because the fix is global to periodic task scheduling. It is not a demo-only patch; it improves sensor, telemetry, heartbeat, and transport polling loops across supported targets.
- Download or clone this repository.
- Put the
ZeroKernelfolder into your Arduino libraries directory:- Linux:
~/Arduino/libraries/ - Windows:
Documents/Arduino/libraries/ - macOS:
~/Documents/Arduino/libraries/
- Linux:
- Restart Arduino IDE.
- Open an example from
File -> Examples -> ZeroKernel.
Add the local library path in platformio.ini:
[env:your_board]
platform = espressif8266
board = d1_mini
framework = arduino
lib_deps =
symlink:///absolute/path/to/ZeroKernelOr vendor the repository inside your project and point lib_extra_dirs to it.
#include <ZeroKernel.h>ZeroKernel.begin(millis);For desktop or custom targets, pass your own clock source:
unsigned long boardClock() {
return millis();
}
ZeroKernel.begin(boardClock);void sampleSensors() {
// Non-blocking work only.
}
void flushTelemetry() {
// Keep this short and cooperative.
}
ZeroKernel.addTask("Sensors", sampleSensors, 100, 5);
ZeroKernel.addTask("Telemetry", flushTelemetry, 500, 10);void loop() {
ZeroKernel.tick();
}String route:
ZeroKernel.publishDeferred("telemetry.temperature", 42);Lean key route:
const zerokernel::Kernel::TopicKey temperatureKey =
zerokernel::Kernel::makeTopicKey("telemetry.temperature");
ZeroKernel.publishDeferredFast(temperatureKey, 42);zerokernel::Kernel::WatchdogPolicy policy = {250, 2, true};
ZeroKernel.setWatchdogPolicy(policy);
ZeroKernel.setTaskHeartbeatTimeout("Sensors", 300);
ZeroKernel.heartbeatTask("Sensors");const zerokernel::Kernel::KernelStats stats = ZeroKernel.getStats();
const zerokernel::Kernel::TimingReport timing = ZeroKernel.getTimingReport();
if (ZeroKernel.isSafeMode()) {
ZeroKernel.exitSafeMode();
}If diagnostics are enabled:
ZeroKernel.dumpStats(printLine);
ZeroKernel.dumpTasks(printLine);
ZeroKernel.dumpTrace(printLine);zerokernel::Kernel::TaskConfig wifiTask = {
"WiFiNode",
pollWifi,
100,
0,
0,
zerokernel::Kernel::kPriorityHigh,
true,
{},
zerokernel::Kernel::kCapNetwork | zerokernel::Kernel::kCapTelemetry};
ZeroKernel.addTask(wifiTask);
ZeroKernel.disableCapabilities(zerokernel::Kernel::kCapNetwork);
// WiFiNode will stay registered but will not be scheduled until the capability is re-enabled.#include <ZeroKernel.h>
void readSensor() {
// Non-blocking work only.
}
void setup() {
ZeroKernel.begin(millis);
ZeroKernel.addTask("SensorReader", readSensor, 500, 10);
}
void loop() {
ZeroKernel.tick();
}Recommended realistic network workload:
-
examples/RealProjectNode: a portable node-style workload that simulates sensor sampling, WiFi link maintenance, HTTP delivery, MQTT delivery, queue pressure, and realistic intermittent transport failures. Latest ESP32 hardware reference:REAL_PROJECT_NODE window_ms=10010 sample_runs=100 fast_avg_lag_us=0 fast_max_lag_us=0 fast_miss=0 link_up=1 wifi_attempts=2 reconnects=1 http_ok=33 http_fail=5 http_rate=86 mqtt_ok=32 mqtt_fail=4 mqtt_rate=88 http_queue=0 mqtt_queue=1 -
examples/ESP32TelemetryNode: a richer ESP32 node example with WiFi maintenance, capability-gated diagnostics, heartbeat events, and periodic runtime summaries. -
examples/ESP32TelemetryBaseline: a manual-loop baseline for the same ESP32 telemetry workload so timing overhead can be compared fairly. -
examples/FaultInjectionDemo: a fault-focused demo that injects overruns, exposes watchdog signals, enters safe mode, and then returns to normal operation. -
examples/FaultInjectionBaseline: a manual-loop baseline for the same fault-focused workload.
Quick runners:
scripts/run_esp32_real_project_demo.sh /dev/ttyUSB1scripts/run_esp32_telemetry_demo.sh /dev/ttyUSB1scripts/run_esp32_telemetry_compare.sh /dev/ttyUSB1scripts/run_fault_injection_demo.sh /dev/ttyUSB0scripts/run_fault_injection_compare.sh /dev/ttyUSB0
ZeroKernel already includes local and hardware validation:
- Desktop regression suite
- Lean profile smoke test
- Desktop benchmark with optional performance gate
- Cross-target compile and resource matrix with budget gates
- Wemos before/after compare with determinism gate
- ESP32 before/after compare with determinism gate
- Wemos diagnostics and level-2 stress with automatic firmware restore
Main scripts:
scripts/run_desktop_tests.shscripts/run_desktop_lean_smoke.shscripts/run_desktop_benchmark.sh --enforce-performancescripts/run_resource_matrix.sh --enforce-budgetscripts/run_wemos_compare.sh /dev/ttyUSB0 --enforce-determinismscripts/run_esp32_compare.sh /dev/ttyUSB1scripts/run_full_audit.sh /dev/ttyUSB0
The runtime can be tuned at compile time through ZeroKernelConfig.h and project-level macros.
Key profiles:
ZEROKERNEL_PROFILE_TINYZEROKERNEL_PROFILE_MINIMAL_RUNTIMEZEROKERNEL_PROFILE_POWER_SAVEZEROKERNEL_PROFILE_LEAN_NETZEROKERNEL_PROFILE_NETWORK_NODEZEROKERNEL_PROFILE_EXTENDEDZEROKERNEL_PROFILE_DIAGNOSTIC
Important lean-build switches:
ZEROKERNEL_ENABLE_LEGACY_LABEL_APIZEROKERNEL_ENABLE_TOPIC_KEY_ONLYZEROKERNEL_ENABLE_EXTENDED_TASK_METRICSZEROKERNEL_ENABLE_DIAGNOSTICSZEROKERNEL_ENABLE_CAPABILITIES
ZEROKERNEL_ENABLE_CAPABILITIES stays enabled in full profiles and is compiled out in POWER_SAVE and MINIMAL_RUNTIME, so lean targets do not pay extra static RAM for capability state.
ZEROKERNEL_PROFILE_NETWORK_NODE biases the runtime toward WiFi/BLE/MQTT-style firmware: key-first routing, bounded command/work queues, stronger drain budgets, and leaner metadata by default.
ZEROKERNEL_PROFILE_LEAN_NET pushes harder on that same direction for module-heavy nodes: smaller queue defaults, stripped diagnostics, topic-key routing, and tighter runtime state for optional network helpers.
ZEROKERNEL_PROFILE_TINYnow also defaults to key-first routing and stripped extended task metrics, so the name better matches the footprint target.ZEROKERNEL_PROFILE_NETWORK_NODEnow keeps capability gating enabled, which aligns the profile with network-aware runtime supervision.ZEROKERNEL_ENABLE_DEBUG_DUMP
For small builds, the goal is to preserve the public API while collapsing runtime cost toward key-based routing and stripped diagnostics.
ZeroKernel is built around explicit runtime constraints:
- No dynamic allocation in the active runtime path
- Fixed-capacity scheduler, queues, and trace buffers
- Non-blocking task callbacks only
- Bounded drain and backpressure policies
- Fault containment through watchdog supervision, recovery, safe mode, and panic routing
- Architecture-specific low-level instructions remain isolated to internal or adapter layers
ZeroKernel is structured to work well as an open-core infrastructure project:
- Open source: kernel core, scheduler, runtime, adapters, validation tooling
- Private / future commercial layers: higher-level deployment, cloud diagnostics, analytics, and enterprise integrations
The repository is now set up as a serious infrastructure codebase, not an Arduino toy project.
ZeroKernel/
src/
core/
diagnostics/
internal/
adapters/
examples/
tests/
benchmarks/
docs/
scripts/
Key files: