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>/* |
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 |
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. 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-nicenano-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.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 checksum discipline as health and admission reports.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.
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 checksumed host-readable report
- 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
- health reports exported through the same host contract as runtime reports
- fixed-layout health reports with checksums for CDC, memory inspection, or another stream readers
- 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.
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. For RA4M1, the native Rust composition implements timebase,
deadline, and USB. The separate Arduino facade delegates clock, deadline, ADC, PWM,
I2C, SPI, and byte I/O to the installed board core and is recorded separately in the
platform matrix. Generic Arduino analogWrite is PWM, not a servo-period provider.
The current RA4M1 clock extends a 48 MHz 32-bit DWT counter, so active firmware must sample it within every approximately 89-second wrap; it does not preserve elapsed time when DWT stops in low-power modes. The 24-bit SysTick deadline provider accepts one-shot delays only through approximately 349 milliseconds. Longer active runs and deadlines need a future always-on/chained provider before stronger timing claims are made.
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.
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.
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 bounds controller-ready and EasyDMA completion polling, retains late-DMA storage in permanently claimed aligned buffers, and propagates terminal direction/endpoint faults through the common error lane. Those finite waits still run inside a critical section and their limits count iterations, not elapsed time. Until target timing and a poll-driven transfer state machine close that gap, they are a liveness containment—not an interrupt-blackout or deadline guarantee. Unsupported nRF isochronous endpoints are rejected during allocation rather than reaching the regular endpoint arrays.
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 crate does not yet 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;ZIGBEE_APSis catalog descriptor metadata only.BleAdvBuilderconstructs advertising packets. It is not a BLE controller/host stack, and a protocol descriptor is not proof that a board implements that protocol.
WiFi join/socket control, BLE scan/connect/GATT control, Zigbee co-processor lifecycle,
shared-radio arbitration, and vendor backend selection remain future work. They will
extend the existing nobro-wireless domain rather than create a parallel link crate.
Each protocol control trait will sit beneath ManagedLink, each logical instance will
select exactly one backend, and board/firmware composition will state vendor-managed
memory, interrupts, coexistence, and radio ownership explicitly. Concrete names and
features become public only when their implementations and exclusivity gates exist.
One trait plus one selected implementation keeps apps backend-agnostic when the whole selection path exists. USB demonstrates that rule now. Future wireless control stacks must earn the same property through explicit composition, ownership, and conformance gates; adding a board profile or a 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.