How the system is layered, the engineering rules that keep it maintainable, and the two modularity mechanisms everything mounts through: backend features (USB, sensors) and the Universal Driver Interface.
NobroRTOS is a layered embedded RTOS architecture focused on maintainability, board compatibility, modular growth, bounded memory, AI robotics integration, and diagnosable recovery.
- Keep the kernel small and contractual.
- Keep hardware facts in board data, not application code.
- Keep device integration behind SAL adapters.
- Keep hot paths static and bounded.
- Keep recovery decisions visible through reports.
- Keep every new hardware-facing feature backed by a software gate.
| Layer | Crate or path | Responsibility |
|---|---|---|
| App | core/apps/<use-case>/* |
Compose board, adapters, manifest, startup graph, and runtime |
| Adapter | core/adapters/<domain>/* or core/adapters/<domain>/<stack-family>/* |
Translate devices or libraries into SAL traits |
| Domain | core/crates/nobro_<domain> |
Shared bounded contracts; no board or external-library ownership |
| SAL | nobro-sal |
Stable service traits for hardware, communication, AI, and edge services |
| Kernel | nobro-kernel |
Admission, quota, IPC, alarms, recovery, health, reports |
| HAL | nobro-hal |
Board profiles, platform traits, leases, timers, PWM, bus, capture |
| Host | nobro-host, host/nobro-host-contract.json |
Report decoding and external contracts |
core/adapters/catalog.json uses stable component IDs to relate contracts,
adapters, external libraries, and host products to domains. Deployment,
maturity, evidence class, supported targets, limitations, and provenance are
orthogonal fields. A component may belong to multiple domains by reference
without duplicating its source or provenance. Board profiles remain outside
this relationship catalog because platform tiers already own board support.
Every component has one primary_domain; additional domain membership is an
alias relationship, never duplicate ownership. Support advances only through
the cataloged, compiled, implemented, physical, promoted, and released states.
The catalog owns the first three states, exact feature bindings own physical
and promoted claims, and versioned package or release manifests own released
claims. The catalog rejects implicit promotion, so a successful target build
cannot become a physical board/module claim.
Board-varying engines use a second, data-only extension boundary in
core/boards/feature_providers.json. It separates the portable contract,
stack family, adapter backend, exact board/firmware binding, resource price,
coexistence policy, evidence, and report wiring. Candidate capability kinds do
not become platform claims: platform_tiers.json accepts a kind only when the
registry contains an exact binding and the ordinary scoped evidence gate.
Each implemented binding also references a complete lifecycle profile, an
executable host-lifecycle receipt, an exact target-build receipt, scoped
physical evidence, and explicit physical limitations. A profile cannot omit a
lifecycle dimension or label a limitation without explaining it.
Disabled bindings must declare the same-board baseline, zero flash/RAM delta,
and forbidden backend symbols. Resource prices share one typed contract across
audio, sensor, and pulse domains. Schema v2 keeps fixed mount ownership
separate from transient heap, stack high-water, CPU, and latency measurements
for one explicit configuration-word sequence, its checked fingerprint, and a
positive operation rate. Default numeric zeroes remain unknown; every exact
binding must classify each fixed and runtime field as measured, source-derived,
or explicitly declared zero. Fixed stack counts provider-created worker stacks;
caller-task high-water remains runtime evidence. Heap-backed driver pools are
retained heap, not opaque vendor-reserved RAM.
Peripheral/controller channels are priced independently from DMA channels and
interrupt slots.
Hardware is described as structured data that can be validated before driver code
relies on it. The current implementation starts with BoardDesc, board features, memory
scripts, and host-readable board profile reports.
BOARD_PROFILES and BOARD_PACKAGES keep the current board
features reviewable from one host build, which makes new board ports easier to
compare before hardware-specific validation begins.
Future board ports should add:
- a board descriptor
- a valid board package
- capacity budgets
- critical pin declarations
- a
HardwareCapabilitySetthroughHalCompatibility - exactly one board feature
- a linker layout
- host report coverage
BoardPackage is the software gate for those facts. It validates non-empty
identifiers, aligned flash origin, non-empty flash/RAM regions, usable capacity
budgets, and distinct critical pins before a port becomes a recommended target.
Firmware can export NOBRO_BOARD_PACKAGE_REPORT so host tooling can inspect the
same contract before manifest and adapter diagnostics.
With the nobro-kernel/hal-profile feature, apps can derive SystemProfile
from BoardPackage, which keeps manifest and admission budgets aligned with
the selected board package.
AI workloads are treated as RTOS-managed modules, not as private background runtimes. A local TinyML model, an attached accelerator, a companion computer, or a third-party API should enter the system through adapter descriptors, capability bits, fixed budgets, caller-owned buffers, timeout policy, and host-readable compatibility reports.
AiInferenceSal is the first SAL contract for this direction. It models a
bounded inference request and result without requiring heap ownership inside the
adapter. Hard-realtime control loops should consume the last valid inference
state or a degraded fallback state instead of blocking on inference.
AI invocation preflight sits before route execution. Rust SAL code and host tooling use the same contract shape to reject oversized input/output buffers, excess scratch or arena RAM, stale snapshot policy violations, degraded fallback, unavailable routes, and open endpoint circuits before a model or remote API is contacted. Host contract checks additionally verify module AI capability declarations because they can see the full application bundle.
AiRoutePolicy adds a small RTOS-side decision layer for local, edge, remote,
and hybrid inference. The policy compares timeout against the caller's budget,
tracks endpoint readiness and consecutive endpoint failures, allows fresh
snapshot reuse, and chooses degraded fallback when the route is not safe for the
current control cycle. Stale snapshot reuse is bounded by the stricter of the
model contract and runtime policy, so cloud APIs and companion-computer
inference remain compatible with real-time control instead of letting network
latency or outdated results leak into critical paths.
ROS and micro-ROS compatibility belongs at the bridge layer. NobroRTOS should
absorb ROS 2's topic, service, action, and parameter concepts, but map them into
bounded queues, fixed request/response records, action state records, and
kernel-owned configuration. DDS, XRCE-DDS, agents, and custom transports should
stay behind StreamSal or RadioSal adapters rather than becoming kernel
dependencies.
RosBridgeSal is the bounded Rust contract for that bridge layer. It reports a
fixed RosBridgeContract summary, uses stable hashes instead of dynamic names
inside realtime paths, and keeps topic publication plus service calls on
caller-owned buffers. DDS, micro-ROS agents, serial bridges, and companion
computer bridges can share this contract without becoming kernel dependencies.
ROS bridge preflight checks topic payloads, service/action response capacity,
queue depth, parameter value size, and timeout budgets before a transport or
agent is contacted.
NobroRTOS offers a bounded executor plus fixed task tables, explicit periods, deadline budgets, mailbox backpressure, and no allocator on critical paths. The graph builder derives the repetitive manifest, admission, capability, and budget wiring while keeping bounded admission visible.
Tock's component isolation and seL4 MCS's mixed-criticality work both reinforce the same rule: critical work needs explicit boundaries and bounded operations. NobroRTOS maps that rule into:
- module criticality
- capability requirements and ownership
- quota-ledger accounting
- deadline contracts
- degraded-mode planning
- fixed event and health reports
- bounded AI and robotics bridge contracts
Fleet releases cross an asymmetric boundary before update policy can use them.
nobro-secure verifies a pinned Ed25519 key, signed image geometry and vectors,
the SHA-256 image measurement, and the rollback floor. Only that operation can
construct VerifiedSignedImage; both fleet rollout and persistent boot staging
consume this private-field token.
Boot trial, confirmation, and revert decisions are committed through a monotonic storage contract before they take effect, and storage errors fail closed. Platform ports own durable flash layout, protected-key implementation, image writing, and the final unsafe jump. HMAC remains appropriate for per-device authentication and authenticated report envelopes, but it is not the fleet firmware-signing authority.
nobro-storage separates record-oriented KV persistence from transactional byte
images. Both use two flash pages, wrap-aware generations, integrity validation,
and a commit-last selection point. nobro-database::PersistentTable feeds its
stable schema image into the transactional blob path using caller-owned scratch
memory. AtomicFileSystem adds a fixed file table, bounded names/data, and
old-or-new power-cut semantics over that same blob path. Board ports define the
reserved pages and implement fallible erase, program, and readback verification.
Recovery is module-scoped first:
- classify the fault
- update health counters
- record a bounded event
- choose an action
- build a fixed-capacity recovery plan
- transition lifecycle state
- export the result through reports
RecoveryPlan converts a recovery outcome into ordered, bounded steps such as
notify, retry, quiesce, restart, heartbeat verification, and resume. The plan
uses caller-provided capacity, reports capacity failures explicitly, and checks
the total recovery budget before a supervisor turns an action into work. This
keeps self-healing deterministic and reviewable without heap allocation.
RecoveryPlanExecution adds a fixed-capacity cursor over that plan so firmware
loops and host simulators can dispatch only time-ready steps, keep overdue work
visible when the caller-provided output buffer is full, and avoid replaying
steps that were already handed to board-specific adapters.
StartupGraph::dependency_impact lets the same recovery path ask which modules
transitively depend on a faulted root module. It returns the affected modules in
reverse startup order, which gives recovery adapters a deterministic
quiesce-before-restart order without heap allocation.
RecoveryPlan::from_outcome_with_impact consumes that impact directly, so a
dependency reboot can pause affected modules, restart and verify the root, and
resume the affected modules in startup order with explicit capacity and budget
checks.
Runtime impact-aware recovery entry points require the caller to pass the
dependency impact explicitly and reject mismatched impact roots, keeping startup
graph ownership outside the hot runtime state while still preserving misuse
detection.
Runtime::apply_recovery_step closes the execution and bookkeeping loop for
dispatched recovery plans. ModuleLifecycleHooks performs platform-owned
notify, retry, quiesce, stop/start, self-test, heartbeat, and resume work. The
runtime updates module state only after the corresponding hook succeeds.
Runtime::reload_module similarly requires ModuleReloadHooks to perform an
actual module-slot unmount/mount and verification; a failed replacement leaves
the module non-active.
Identical fault work is bounded by RecoveryStormPolicy: health counters still
record every occurrence, but duplicate event/lifecycle/recovery-plan dispatch is
coalesced during the cooldown. Action escalation, error changes, cooldown expiry,
and a healthy record re-open dispatch, preserving first-fault evidence without
hiding worsening health.
Disabled modules lose mailbox traffic, alarms, quota reservations, watchdog registrations, and runtime authorization. Repeated disable commands are idempotent at the runtime API boundary.
Foreign C/C++ modules use a narrower enforced boundary. ForeignModuleRunner
owns admission and callback state, while ForeignHostContext binds the admitted
identity to every host operation and combines capability authorization, call/byte
quota charging, execution, and bounded trace records. The foreign caller cannot
supply a ModuleId; denial or quota exhaustion prevents the protected operation.
Default rules:
- no heap on hot paths
- fixed-capacity manifests, graphs, quota ledgers, mailboxes, alarms, logs, and reports
Sampletickets instead of cross-crate heap buffers- compile-time feature selection instead of runtime plugin loading
- explicit cleanup when modules are disabled
- caller-owned or pool-owned buffers for AI input/output
- fixed message history for ROS-style bridge queues
Any future allocator must be feature-gated, documented, and excluded from hard-realtime paths.
This document turns the project route into engineering rules that can survive new boards, new adapters, and long maintenance windows.
- Board configuration is data:
BoardDesc,BusLayout, and Cargo features are validated before applications depend on them. - Hot paths avoid allocation through static pools and fixed-capacity structures.
- Kernel, HAL, SAL, adapters, and apps exchange public contracts rather than private state.
- Deadline slots, admission, and recovery policy stay in the kernel instead of being duplicated in drivers.
| Layer | Rule |
|---|---|
| App | Assembles features and owns policy wiring; it should not touch registers directly. |
| Adapter | Translates one device or library into SAL traits; no private scheduler or heap. |
| SAL | Stable capability surface: bus, stream, radio, actuator, sensor, crypto. |
| Kernel | Deadline slots, health, sample tickets, error policy, and admission gates. |
| HAL | Board layout, register access, event capture, PWM, bus, and leases. |
Board compatibility must be data-first:
- Each board exposes a
BoardDesc. - Each board exposes a
BoardCapacityso flash, RAM, sample-pool, and module limits can be checked before hardware bring-up. - Each bootloader layout has an explicit Cargo feature and linker script.
- HAL feature selection must enable exactly one
platform-*feature and oneboard-*feature. - App and adapter crates must disable default features on HAL dependencies and
re-enable board features explicitly. This keeps
board-promicro-nosdfrom leaking intoboard-promicro-s140builds through dependency defaults. - Hardware parity checks read registers back into snapshot structs.
- Host tools consume
nobro-hostconstants orhost/nobro-host-contract.jsonrather than duplicating magic values. - Host tools should decode module tags and capability bits through the shared
nobro-hosthelpers or the JSON contract, so reports stay readable as more boards and adapters are added. - Host tools should summarize boot diagnostics in this order: board profile, board package, manifest, adapter compatibility, admission, then runtime. This keeps first-fault guidance stable as more reports are added.
The current nRF52840 backend uses PPI. The portable HAL term is
HalEventCapture; do not leak "PPI" into app or adapter APIs unless the code is
nRF-specific.
Every module should have one of these roles:
- time-critical kernel primitive
- portable HAL capability
- SAL trait definition
- thin adapter
- app composition
- host contract or validation helper
If a module wants to do two of these at once, split it before adding features.
The kernel owns the static SystemManifest model. A manifest describes each
module's criticality, capability requirements, capability ownership, memory
budget, deadline contract, and fault thresholds. It is intentionally a no-heap
data structure so host tests can validate partitioning before any firmware is
flashed.
Each manifest can produce a stable fingerprint over module IDs, criticality, capability contracts, memory budgets, deadline contracts, and fault thresholds. That gives host tools a compact way to compare static system graphs without serializing the full manifest.
SystemProfile adds board-class limits for flash, RAM, sample-pool slots, and
module count. This lets NobroRTOS reject a feature set that does not fit the target
board before a linker script or flashing step gets involved.
Apps should use kernel_module_spec when assembling manifests so kernel-owned
capabilities stay consistent across demos and board ports. Apps can seed
StartupGraph directly from a SystemManifest, then add only the dependency
edges that are specific to the application boot path.
Fault handling is intentionally small:
KernelErrorclassifies failures;HealthFaultadds source, subsystem code, and two bounded detail words.Actiondescribes recovery without allocating memory.HealthMonitortracks per-module consecutive failures and their full latest context;FaultPolicymay retain state and decide per module.FaultThresholdsescalates from local retry, to user notification, to module reboot.EventLogkeeps a fixed-size ring of health, recovery, overrun, manifest, and host events for post-fault inspection.Supervisorties health counters and event records together so every recovery decision leaves a bounded audit trail.Watchdogtracks module heartbeats in software so liveness rules can be tested without binding NobroRTOS to one hardware watchdog block. Disabling a module removes its watchdog registration so later sweeps cannot revive stale liveness faults.ModuleRuntimeGuardtracks fixed-slot module states across Active, Suspended, Faulted, Recovering, and Disabled paths so recovery and later device-power policy share one control-plane model.KernelExecutorownsExecutorPower: measured poll duration feeds the per-module energy ledger, and authoritative next-activity time drives a fallible wake-program/sleep-entry hook. Executor suspension and resume call peripheral power hooks before committing module state.Lifecycledefines legal boot, running, degraded, recovering, and halted transitions so recovery paths are explicit and testable.DegradePlannerkeeps System and HardRealtime modules enabled while shedding lower-criticality modules to fit board-class budgets.RetryPolicyandRetryStatemake bounded retry behavior explicit instead of embedding ad hoc loops in adapters.StartupGraphandStartupPlannermake module dependency order explicit, map module IDs to compact dependency bits, and reject duplicate dependency edges or cycles before firmware boot logic is involved.QuotaLedgerconverts manifest budgets into fixed-capacity runtime accounting, so modules can reserve and release RAM, flash, and pool slots without heap allocation. Disabling a module resets its runtime quota usage so degraded mode returns resources to the system profile immediately. Runtime quota mutations are rejected for disabled modules.CapabilityGrantTablederives runtime authorization from manifest requirements and ownership, keeping module access checks fixed-capacity and testable. Runtime authorization is still gated by module state, so disabled modules cannot keep using previously admitted capabilities.Mailboxprovides fixed-capacity control-message IPC with accountable module quotas, reserved recovery/shutdown capacity, and priority ahead of ordinary FIFO traffic; data payloads still move throughSampletickets and static pools. Runtime IPC validates both message endpoints against the admitted and enabled module set before messages enter the queue. Disabling a module purges queued messages to or from that module so stale control traffic cannot outlive the module state transition.AlarmQueueprovides no-heap one-shot and periodic software timers without binding app logic to a specific hardware timer block. Disabling a module also removes its queued alarms so disabled modules cannot be reawakened by stale timer events, and new alarm scheduling is rejected for disabled modules.AlarmDispatchsummarizes due-alarm delivery, including partial progress and the first alarm blocked by mailbox backpressure, without dropping the alarm. Runtime code can route that blocked alarm through recovery as a deadline fault, keeping timer backpressure visible to health reports.KvStoreis the kernel's volatile fixed-capacity configuration table. Durable records and typed database images usenobro-storageandPersistentTable; a port still has to reserve flash pages and implement fallible erase/program/readback.AdmissionControllercomposes manifest validation, startup ordering, and quota seeding into one boot-time software gate before board-specific startup code runs.TaskDeclandDeadlineContractcarry explicit phase, period, and relative deadline. The release queue uses phase to avoid unnecessary bursts, while shared admission deliberately keeps pessimistic interference instead of treating offsets as free schedulability headroom.AppGraphis the fluent graph-authoring path.GraphSpecborrows const/staticTaskDeclandChannelDeclslices for firmware that cannot afford a capacity-sized graph builder on the startup stack; both paths expand through the same manifest/startup/profile validation.GraphSpec::start_executoradditionally performs boot, task registration, and sealing with only the derived manifest/startup nodes as temporary scratch; task metadata is regenerated from the declaration, so applications that do not need runtime graph introspection avoid retainingBuiltGraphRAM or its label/reactor arrays on the startup stack. This is an explicit static-RAM-versus-startup-stack composition choice, not a claim that the temporary form always has the lower peak on every target.- Opt-in P-ISR admission prices bounded interrupt execution, platform-reserved
priorities, higher-priority interference, and nested exception-stack use.
InterruptHandofflimits ISR work to lock-free ready/event publication. - Opt-in P-SLICE uses task-owned PSP stacks and
SliceController; the nRFcortex-m-sliceport saves R4-R11, EXC_RETURN, BASEPRI, and the lazy-FPU extension in PendSV. PendSV is configured at or below the selected kernel BASEPRI ceiling so it cannot split a process-wide critical-section transaction. A queued forced switch is committed only after the port reports the PSP switch complete, so a ceiling-held overrun keeps the old task and sentinel attribution until the section exits; a section that never exits requires watchdog escalation. After a forced switch commits,ForcedSuspendHandoffpublishes the suspended module to privileged dispatcher context.route_forced_suspendthen requests module-scoped recovery; conflicting module identities fail closed instead of recovering the wrong module. The current port is a bare-nRF profile: combiningcortex-m-slicewithboard-promicro-s140fails at compile time because it programs PendSV through CMSIS and does not yet integrate interrupt control through the SoftDevice API. Cooperative execution remains the default and cannot forcibly suspend a non-yielding poll. Privileged slices are not an isolation boundary. Every port other than the explicitly MPU-capable bare-nRF composition rejects an unprivileged slice at admission. - Native nRF board features install one BASEPRI-backed implementation for the
ecosystem-wide
critical-sectionABI. Bare builds mask logical priorities 3-7 and S140 builds mask application priorities 6-7; deadline/watchdog/P-ISR domains above those ceilings remain live and use lock-free handoff only. The build gate rejects a simultaneous Cortex-M PRIMASK implementation. - The nRF idle path uses SEVONPEND plus
SEV; WFE, rechecks the deadline and lock-free ISR handoff, then performs the sleepingWFE. This consumes stale events without reopening the check-to-sleep race or counting a false sleep. - Cortex-M0+ has no BASEPRI. The SAMD21 port therefore owns a measured PRIMASK provider: a free-running SysTick counter records each outer section, nested sections inherit that interval, and a counter wrap fails closed. Its serial report exposes maximum masked cycles/time and the configured bound; target compilation alone is not physical timing evidence. This measurement owns SysTick in the current SAMD21 core-only port.
AdmissionReportprovides a fixed host-readable admission result so startup failures can be diagnosed without dynamic logging. It can be built from the same admission result used by boot code, reducing report-path drift.BootAssemblyis a no-heap startup facade for small applications. It builds a manifest from static module specs, applies explicit startup dependencies, runs admission, constructs the runtime, boots it toRunning, and preserves manifest/admission reports without hiding the failing phase.RecoveryCoordinatorcomposes health, lifecycle transitions, watchdog-style deadline faults, and event logging into one testable recovery path.HealthReportturns supervisor snapshots into fixed-layout host-readable records with the same diagnostic-checksum discipline as health and admission reports; this remains accidental-corruption detection, not authentication.EventLogReportsummarizes the fixed event ring for host tools, including capacity, drops, and the latest event's module, severity, kind, and payload.ModuleRuntimeReportsummarizes module runtime states for host tools, including Active, Suspended, Faulted, Recovering, Disabled, and the latest changed module.DegradeApplicationReportsummarizes the latest runtime degraded-mode application, including requested disables, newly disabled modules, modules that were already disabled, the budget reason, and the application timestamp.RuntimeReportsummarizes runtime control-plane state, including lifecycle state, mailbox pressure, alarm schedule, KV writes, quota usage, and event-log pressure.BoardProfileReportexports the selected platform, board package, flash origin, board-class budgets, and critical pins as a fixed host-readable record before any hardware-specific probe is needed.ManifestReportexports manifest validity, static graph fingerprint, required and owned capability bits, budget use, and validation error context.AdapterCompatibilityReportprovides an admission-before-admission gate for SAL adapters. It records adapter count, required and owned capability bits, static budget use, and compatibility error context in a host-readable layout.AdapterPreflightkeeps the first adapter assembly error so duplicate module IDs or fixed-capacity overflow can still be exported as compatibility reports.Runtimeassembles an admitted plan with mailbox IPC, alarms, kernel KV, and recovery into one fixed-capacity control plane for apps and adapters. It can be constructed from an admitted plan or directly from a manifest plus startup graph, and routes software watchdog expiry through the same recovery and health-report path as explicit module faults. Runtime quota helpers keep reserve/release accounting on the admittedQuotaLedgerso memory discipline continues after boot. Module recovery completion is also explicit: the runtime returns through driver initialization, records a healthy heartbeat, and only then resumesRunning; disabled modules are rejected before any lifecycle transition is attempted. Degraded-mode decisions are validated before module state changes and the last successful application is retained as a fixed-layout host report. Runtime assembly from startup plans is fallible, so fixed-capacity module registration errors are reported instead of being silently ignored. Manual runtime disable is idempotent at the runtime API boundary, keeping repeated recovery commands safe while the lower module state machine remains strict.- Alarm, KV, retained event-log, and capability-trace capacities are optional
composition choices and may be zero. Their existing operations fail closed
on zero capacity; mailbox IPC and per-module admission/health tables remain
mandatory.
LeanRuntimeandLeanKernelExecutorCellexpose this composition without asking users to spell seven or eight const parameters.
Recovery is module-scoped by default. Full chip reset is a last resort and should remain outside hot-path adapters.
The default rule is no allocator:
- use static pools for payloads
- pass
Sampletickets instead of raw buffers across crates - use
LeaseGuardto avoid resource leaks - keep ISR work bounded and defer parsing to cooperative tasks
- prefer compile-time features over runtime plugin registries
Any future heap use must be feature-gated, documented, and excluded from hard-real-time paths.
Before adding a hardware-dependent feature, add at least one software gate:
- a host unit test
- a diagnostic-checksummed host-readable report (accidental-corruption detection only)
- a register snapshot comparison
- a board feature/linker validation
- a no-hardware stub adapter
This keeps NobroRTOS useful throughout design, review, and bring-up.
The next step is not a larger kernel; it is stronger contracts:
- board manifests generated from board descriptions
- board profile reports exported by apps so host tools can verify the selected board class before interpreting adapter and runtime reports
- adapter manifests generated by feature selection
- adapters expose
AdapterManifestdata so app assembly can feed the kernel admission controller without hand-written module budgets - adapters expose
AdapterDescriptorsummaries derived from their manifest so host or app compatibility checks can inspect module ID, capability bits, and budget without parsing adapter internals - adapter descriptor sets expose fixed-buffer descriptor copy and module lookup APIs so host/app tooling can inspect adapter inventory without heap allocation
- adapter descriptor sets can be checked before admission for duplicate module IDs, exclusive capability ownership conflicts, board-class budget fit, and module-count limits
- adapter descriptor sets should export a fixed-layout compatibility report before app admission so board bring-up can diagnose adapter/profile mismatch without hardware-specific probes
- compile-time or host-time checks for RAM, flash, capabilities, and criticality
- optional async executors with static task allocation, deadline-guarded futures that turn fired compares into typed health faults, and graph-linked multi-priority reactor domains with one admitted driver task per domain plus explicit cross-domain channels
- health reports exported through the same host contract as runtime reports
- fixed-layout health reports with explicitly unkeyed diagnostic checksums for CDC, memory inspection, or other local readers; untrusted transports require the authenticated report envelope
- app assembly patterns that connect adapter preflight, board package reports,
and
BootAssemblywithout adding runtime plugin registries
The current executor support is deliberately small: TaskTable is a fixed-size
task registry that records period, budget, criticality, due time, and overrun
statistics. An intrusive sorted release queue makes the next deadline an O(1) lookup;
elapsed releases are transferred without a capacity-wide scan. A five-level
criticality bitmap selects a FIFO head in O(1), preserving fairness between peers.
Bounded reinsertion happens after a poll, outside the release-to-dispatch edge.
The ready-membership word supports at most 32 tasks and rejects a wider table.
Bookkeeping remains synchronous in the current executor: poll timing, overrun
handling, reinsertion, idle-safety checks, and instrumentation snapshots finish
inline before the cycle can report idle. NobroRTOS does not yet ship an admitted
maintenance-service reserve or saturated debt model, so no background
accounting task is claimed.
Compare providers can program the exact earliest release group and transfer its
bits from ISR context; early, duplicate, and stale bits fail closed.
Tickless admission charges a measured compare-wake-to-dispatch bound once in each response-time calculation. The bound defaults to zero for compatibility and must be set explicitly when the selected board/composition has measured evidence.
The current observability support is equally small: EventLog is a no-heap ring
buffer that preserves the latest records, tracks drops, and can be copied into a
host-readable report without exposing dynamic logging dependencies to ISR or
hard-real-time code.
PlatformHal identifies a platform and board package; it does not require a
monolithic set of peripherals. Timebase, scheduling, deadline, capture, PWM,
lease, I2C, and SPI behavior are independent provider traits.
Portable leases use a neutral class plus instance number, and each platform
adapter performs the concrete peripheral mapping. Bus providers also declare
whether transfers are polling or DMA.
Capability rows describe one firmware composition, not the union of every API that can
compile for a board. The native RA4M1 composition implements a LOCO-clocked AGT
timebase/deadline, GPT2/ELC event capture, bounded ADC/PWM/I2C/SPI/UART, USBFS,
reset, ordinary CPU sleep, and generation-safe leases. The separate Arduino facade
delegates its providers to the installed board core and is recorded separately in the
platform matrix. Generic Arduino analogWrite is PWM, not a servo-period provider.
AGT0 extends 16-bit LOCO underflows into a 64-bit monotonic clock and continues through
ordinary Cortex-M WFI. AGT1 rounds deadlines up to the next LOCO tick and chains
long waits without exposing the raw 16-bit ceiling. The power provider clears
SLEEPDEEP, rejects masked-interrupt entry and active controller vetoes, and therefore
does not imply standby/deep-standby retention.
NobroRTOS uses mountable backends where the subsystem implements the complete selection contract: a firmware composition chooses one implementation of a common trait and application code consumes the trait. USB is the implemented reference. Sensor categories use the Universal Driver Interface described below. Wireless has a shared bounded data-plane trait today, but its vendor-stack selection layer is planned, not present.
Arduino sketches use the stacks and peripheral ownership supplied by the installed ArduinoNRF board package. Native Rust firmware uses the providers compiled into its selected composition. These are distinct compositions; NobroRTOS does not currently offer a feature that swaps a running native firmware to an Arduino-managed BLE stack.
UsbStack trait + typed try_mount() (with panic-compatible mount()); a board picks
one backend:
| feature | backend | status |
|---|---|---|
backend-nrf-usbd (default) |
vendored nrf-usbd + usbd-serial CDC |
implemented |
backend-usb-serial-jtag-esp32c3 |
ESP32-C3 fixed-function USB serial/JTAG register map | implemented; physical recovery evidence pending |
backend-usb-serial-jtag-esp32s3 |
ESP32-S3 fixed-function USB serial/JTAG register map | implemented; physical recovery evidence pending |
backend-ra-usbfs |
USBFS CDC device backend | implemented |
The Cortex-M usb_stack_demo consumes only try_mount() + UsbStack and selects nRF
USBD or RA4M1 USBFS. ESP32-C3/S3 use architecture-specific demos in their port crates;
the Cortex-M package does not advertise impossible RISC-V/Xtensa features. Compile-time
guards allow exactly one implemented backend. Its RA feature uses the exact exported
identity and the 0x4000 RA4M1 application link map, but it is a compile/link contract;
complete UNO R4 clock/mux startup remains in the RA4M1 port executable. Mount preflight
validates fixed descriptor requirements before a process-wide permanent claim and before
hardware access; the panic-compatible mount() wrapper is retained for existing callers.
Nonfunctional placeholder stacks are not published as features.
The UNO R4 composition has a separate board-route lifecycle around the generic
RA4M1 backend. Its connector starts on the onboard ESP32-S3 upload/debug
bridge; NOBRO_NATIVE selects RA4M1 USBFS after the bridge status path is
ready. A never-configured native attempt falls back after a bounded deadline,
while a configured session remains active. NOBRO_BRIDGE disconnects native
D+, performs the stock bootloader-compatible P408 high-to-low pulse, and
returns P408 to input. This board operation is intentionally not generalized
into controller-only UsbStack recovery.
UsbConfig is the requested identity. The nRF backend generates descriptors from it,
the RA4M1 backend accepts only RA4M1_USB_CONFIG, and ESP USB-Serial-JTAG descriptors
are fixed by the controller and ignore the request. The public identity_policy() and
config_supported() facts keep accepted configuration distinct from host-visible
identity. capabilities() and the immutable successful-mount receipt bind one logical
stack instance to the backend id, advertised-identity class, MTU, buffer/service limits,
lifecycle generation, and supported recovery operations.
The ESP link state fails closed: SOF establishes only a live bus. The backend clears
reset-high SERIAL_IN_EMPTY without treating it as enumeration evidence, waits for a
free IN FIFO before issuing a zero-length EP1 probe, and reports Configured only after
a later EP1 IN token or OUT packet. Bus reset and the SOF watchdog invalidate that state.
Reset/pre-probe OUT evidence is cleared and its packet FIFO is drained with a 64-byte
per-poll bound before probing, so stale data cannot strand the FIFO or become configured
evidence. IN_EP_DATA_FREE means only that the FIFO is not full; nonempty writes clear
the old empty event and retain a pending flag until a later SERIAL_IN_EMPTY event.
Therefore flush never infers completion from free capacity. Host state-machine tests do
not replace physical disconnect/reconnect evidence.
The nRF backend advances controller readiness through non-blocking lifecycle states. EasyDMA
setup and completion bookkeeping use short critical sections, but completion waits run with
interrupts enabled under one atomic controller claim. A board monotonic clock supplies a
10 ms elapsed-time limit; without it, the driver exposes only a conservative poll-count
fallback and makes no time claim. Permanent aligned staging buffers remain quarantined after
a terminal timeout, so late hardware completion cannot access caller storage. The optional
nrf-timing-diagnostics feature reports clocked/fallback samples and maximum wait/masked
times without charging normal images for those counters. Unsupported nRF isochronous
endpoints are rejected during allocation rather than reaching regular endpoint arrays.
nobro-audio defines allocation-free codec configuration, lifecycle,
capture/playback, an optional measured-I/O deadline extension, explicit
admission price, and a fixed-capacity backpressuring frame ring. Fixed mount
ownership is separate from transient heap, stack high-water, CPU, and latency
evidence for an exact format/frame/rate workload. It does not
reimplement a vendor DMA engine.
The first concrete bridge is audio/esp32s3-es8311. Its Rust side validates
signed-16 formats, frame bounds, state transitions, recovery, and resource
accounting over a mountable transport. Its Arduino side,
NobroNiusAudio.h, wraps the pinned NiusAudio ES8311 driver with a
compile-time queue. Arduino-ESP32 owns I2S/DMA and codec control; Nobro owns
what the application can submit, how much it retains, the completion budget,
backpressure, and diagnostics.
The first exact Arduino binding is deliberately narrow: 16 kHz mono signed-16 full-duplex audio, 192-byte maximum frames, two queue slots, and 100 transfers per second. The registry prices the isolated link delta, retained heap, caller-stack high-water, active cycles, p99/max latency, two I2S directions, and their two GDMA/interrupt paths. Repeated recovery and physical playback/capture pass; this does not price another codec or format.
NiusAudio is a member of nobro-audio, not a parallel ecosystem or copied
source tree. A new codec remains a module/library implementation under the
same contract. A board claim appears only when the feature registry has an
exact backend, composition binding, price, disabled-symbol proof, report
wiring, and executable evidence.
Continuous sampling and hardware pulse generation reuse the existing
nobro-sensors and nobro-servo domains. They do not create another
ecosystem or a board-specific application hierarchy:
sensors/esp32-adc-continuousmounts the Arduino-ESP32 process-wide continuous ADC/DMA service behind bounded channel/frame, deadline, lifecycle, recovery, and configuration-bound resource-price contracts.servo/esp32-ledcmounts fixed-frequency duty output.servo/esp32-rmtmounts bounded pulse-symbol output.
NobroEsp32Peripherals.h is the corresponding beginner-facing composition.
It offers two interchangeable ADC transports with the same bounded lifecycle:
Esp32ContinuousAdcuses Arduino's compact convenience path.Esp32PersistentContinuousAdc<Pins, Conversions>reads through the public ESP-IDF API into DMA-aligned object storage and performs no per-frame heap allocation.
The persistent path is opt-in, so an unused transport contributes no linked code. A provider price separates retained ownership from transient peak, stack high-water, CPU cycles, and p99/maximum latency for one exact configuration and admitted call rate. The adapter rejects a requested configuration or declared rate that differs from the evidence; the scheduler remains responsible for enforcing that admitted call rate at runtime. The ADC facade reports the aligned conversions per channel required by the board core and rejects a frame shape that Arduino- ESP32 would silently widen, keeping averaging and deadline semantics exact. State-restoring classic ESP32, single-core ESP32-C3, and dual-core ESP32-P4 campaigns verify continuous sampling, LEDC frequency/duty, RMT pulse timing, lifecycle recovery/release, and immediate runtime reservations. C3 measured 19,999 conversions/s, 1,002 Hz at 249 permille, and 499-500 us RMT levels. P4 measured 19,795 conversions/s with an exact aligned frame, 1,002 Hz at 249 permille, and 499-500 us RMT levels.
The configuration-bound C3/P4 comparison used three runs per transport. The heap monitor itself consumes 36 bytes; values in parentheses subtract that common floor.
| Workload | Transport | transient heap | task stack high-water | worst cycles/read | p99 / max |
|---|---|---|---|---|---|
| 1 channel, 12-bit, 16 conversions, 20 kHz | Arduino C3 | 116 B (80 B) | 1,376 B | 5,338 | 803 / 803 us |
| same | persistent C3 | 36 B (0 B) | 1,416 B | 3,268 | 803 / 803 us |
| 1 channel, 12-bit, 32 conversions, 20 kHz | Arduino P4 | 180 B (144 B) | 1,476 B | 11,220 | 1,601 / 1,601 us |
| same | persistent P4 | 36 B (0 B) | 1,668 B | 6,756 | 1,601 / 1,601 us |
All runs preserved the largest free block and returned to their pre-batch heap value. Interleaved ADC, LEDC, and RMT kept the ADC latency envelope while the persistent path reduced worst composite active cycles by about 20% on both targets. An equivalent S3 application build prices the compact ADC path at 21,108 B flash / 368 B static RAM and the persistent path at 20,520 B / 456 B; the disabled build remains byte-for-byte equal to baseline. Unreferenced ADC inputs are not calibration evidence. S3 remains target-build evidence only. No exact binding is promoted until fixed ownership and the remaining provenance fields are complete; compilation never turns an unknown price into zero.
Provider lifecycle distinguishes temporary quiescence from unmount:
quiesce preserves logical configuration for recover, while release
stops/deinitializes or detaches the transport, forgets configuration, and
returns Down. Rust adapters and the Arduino facade share this rule so a
detachable module cannot retain vendor resources by API accident.
Implemented today in nobro-wireless:
WirelessBackendis the bounded application data plane, andManagedLinkadds resource accounting plus a deadline check for one immediate send attempt.TxContractdoes not schedule priority or execute retries: priority belongs to the scheduler and retry state belongs to the caller. Implementations are constructed explicitly; the portable crate does not select a vendor stack from a board profile.Mfrc522<SpiIo>implements bounded ISO 14443A UID polling, andCc2530<ByteIo>implements an initialized raw IEEE 802.15.4 PSDU transport behindWirelessBackend, bounded by the 127-byte PHY frame limit. It is not a Zigbee join/network/APS stack. The optionalzigbee-apsfeature implements bounded, unfragmented APS data frames only over a separately supplied, already joined NWK backend; it does not add formation, joining, ZDO, security, or fragmentation.BleAdvBuilderconstructs advertising packets. It is not a BLE controller/host stack, and a protocol descriptor is not proof that a board implements that protocol.WifiStackandBleStackare distinct allocation-free lifecycle contracts beneath the common data plane.MountedWifi/MountedBleown fallible mounting; credentials remain runtime-only, andBleEventQueuebounds callback-to-task transfer.NativeNetworkStack/MountedNetworkform a separate owned TCP/UDP/DNS data plane with stable socket, MTU, buffer, protocol, and lifecycle limits.wireless/wifi/arduino-wifis3andNobroArduinoWiFiS3.hform the first concrete WiFiS3 bridge. The facade owns no credentials or heap, copies at most the caller's scan capacity, and composes the separately mounted fixed-slotArduinoWiFiS3Network. The UNO R4 board core still owns its process-wide UART/coprocessor stack, blocking calls, dynamic strings, sockets, and controller resources. One exact WiFiS3 0.6.0 binding has zero-disabled, state-restoring association/DNS/TCP/lifecycle evidence and a complete RA-side/controller-image price at one HTTP operation/s; controller-internal runtime remains separate. Registry controller evidence pins the official artifact at 1,180,064 B application flash / 64,628 B static RAM and records source minima of 22,288 B / three persistent application/USB task stacks.wireless/wifi/arduino-espandNobroArduinoEspWiFi.hadd the same bounded station lifecycle for ESP32, ESP32-C3, and ESP32-S3 through the pinned Arduino-ESP32 3.3.10 board package.ArduinoEspNativeNetworksupplies the same independently mounted fixed-slot data-plane contract. The facade disables credential persistence for its calls and retains no credentials, but ESP-IDF still owns process-wide radio, event-loop, TCP/IP, task, socket, and heap resources. The exact C3 path has state-restoring association, DNS, TCP, and lifecycle evidence plus one complete configuration price at four HTTP transactions/s. That price does not transfer to another board, rate, socket workload, or WiFi/BLE composition.wireless/ble/arduino-bleandNobroArduinoBLE.hadd a bounded one-service/one-characteristic peripheral over ArduinoBLE 2.1.0. The exact UNO R4 build follows ArduinoBLE's officialHCIVirtualTransportATinto the installed board package's WiFiS3 modem rather than duplicating a controller driver. One global mounted facade is admitted because ArduinoBLE owns process-wide HCI/GATT state and heap. The facade supplies the missing ArduinoBLE 2.1.0 UNO R4HCIENDpath, compensates only the observed cleared-service retain, and exposes provider disconnect. Three exact physical cycles cover GATT write/read/notify, disconnect, remount/recovery, and a connected subscribed link across WiFiS3 DNS/TCP traffic. Synchronous modem calls still serialize RA-side servicing; controller retained/transient heap, complete task/stack reservations, and CPU pricing remain open.wireless/ble/arduino-espandNobroArduinoEspBLE.hreuse the BLE library bundled with Arduino-ESP32 3.3.10. Classic ESP32 keeps the package-selected Bluedroid host; ESP32-C3/S3 keep NimBLE. The facade bounds callbacks to a four-event fixed ring and reports overflow rather than allocating a Nobro queue. All three disabled targets equal their baselines and all three enabled targets compile. Exact ESP32-C3 and classic ESP32 bindings each have two independent eight-cycle physical GATT/quiesce/recovery campaigns during admitted WiFi DNS/TCP traffic and bounded post-warmup heap plateaus. C3 has a composition-scoped incremental BLE price over its separately priced WiFi binding; classic ESP32 has a whole-composition price only because its standalone WiFi baseline is not separately priced. ESP32-S3 and other workloads remain open.
Additional WiFi backends/workloads, BLE physical/resource closure, Zigbee
co-processor lifecycle, shared-radio arbitration, and prices for unmeasured
compositions remain future work.
They extend the existing nobro-wireless domain rather than create a parallel link
crate. Each logical instance selects exactly one backend, while WiFi and BLE instances
may coexist when board composition explicitly admits shared memory, interrupts,
coexistence, and radio ownership. A portable trait is not a board-support claim.
MulticoreExecutorLifecycle owns the fixed-capacity placement and lifecycle
state for the per-core executors. transfer_executor() is the transactional
runtime path: it validates both live-core states, ownership, destination slots,
and utilization before asking the source executor to move work. The
KernelExecutor implementation then:
- requires both task sets to be sealed and the destination runtime to admit the module;
- reruns response-time admission over the prospective destination set;
- moves the task's exact release phase, ready state, statistics, and future power attribution; and
- commits placement/accounting only after the executor move succeeds.
An admission or capacity failure leaves both task tables and the placement unchanged. Historical CPU and energy entries remain attributed to the core that measured them; future entries accrue on the new owner. Runtime objects and peripheral handles are not byte-copied between cores. They must be pre-admitted on both executors or live in the bounded cross-core transport selected by the composition.
Call the transfer only at a platform synchronization barrier where neither core is polling the two executors. Safe Rust's two mutable executor borrows express exclusive access inside the transfer, but starting and stopping the physical peer core remains a board-port responsibility.
One trait plus one selected implementation keeps apps backend-agnostic when the whole selection path exists. USB demonstrates that rule now. Wireless backends must earn the same property through explicit composition, ownership, and conformance gates; adding a portable trait, board profile, or catalog descriptor alone is insufficient.
NobroRTOS treats drivers the way Adafruit Unified Sensor treats sensors: one category, one trait, many mountable backends. A part is catalog data; a backend is a compile-time feature that plugs a concrete library or transport behind the same SAL trait.
This is the public rule behind the ImuSal backend example (udi_imu_demo) and
the pattern to extend to other sensor categories.
Category trait (SAL) e.g. ImuSal::sample()
├─ backend-native register driver in-tree (mpu9250-imu)
├─ backend-eh any embedded-hal driver crate
├─ backend-c-module C/C++ module via nobro_app.h
└─ backend-arduino stock Arduino library via NobroArduinoShim
Every backend:
- Implements the same category trait (
ImuSal,TempSal, more to come). - Is selected by exactly one
backend-*Cargo feature (mutual exclusion). - Carries a stable
backend_idin the health report so the selected transport remains visible without the diagnostic function naming a driver. - Runs through the same diagnostic body — only the mount changes.
| From your existing code | UDI answer |
|---|---|
| Arduino sensor library | backend-arduino shim behind the category trait |
embedded-hal driver crate |
backend-eh adapter |
| Register-level C driver | backend-c-module via nobro_app.h |
| In-tree Nobro driver | backend-native |
| Task / loop / executor | NobroRTOS module + manifest (see cookbooks) |
core/apps/imu/udi_imu_demo shares one app.rs diagnostic body across three binaries:
| Backend | Feature | backend_id |
Transport |
|---|---|---|---|
| Native HAL | backend-native |
1 | SPI via nobro_hal |
| embedded-hal | backend-eh |
2 | SPI via SpiDevice |
| Arduino shim | backend-arduino |
3 | SPI via NobroArduinoShim + stock MPU9250 class |
The three feature-selected binaries share the same application body and report contract. Each backend must preserve the same public status fields and backend identifier.
- Define a category trait in
nobro_salwith bounded return types (no heap). - Add a catalog entry in
nobro_device(part id, bus, who-am-i, ranges). - Ship at least two backends to preserve the swappable contract.
- Add a swap demo app with one shared diagnostic body and feature-gated mounts.
- Add portable contract checks and exercise the selected backend before claiming support.
- Implement the category trait in a new adapter crate or C/C++ module.
- Add a
backend-*feature withcompile_error!if more than one is enabled. - Wire the mount in the demo app's
main.rs(thin — only constructs the backend). - Flash and read the report;
backend_idmust be unique and documented.
- PORTING.md — migration cookbooks and board-port workflow
- GETTING_STARTED.md — public host and image-deployment workflow
The rule generalizes: TempSal::read_temp_centi_c() reports centi-degrees Celsius from
whatever part a backend wraps. All three udi_imu_demo backends implement it against the
same die-temperature register, sealed on the same board back to back:
| Backend | backend_id |
temp reading |
|---|---|---|
| native HAL | 1 | 31.24 C |
| embedded-hal | 2 | 31.20 C |
| Arduino-library shim | 3 | 31.15 C |
Three transports, one silicon, answers within 0.1 C - the category abstraction costs
nothing in fidelity. The report's temp_centi_c field and its 10-60 C plausibility
check are part of all_pass.