GitHub

Docs / Guides / The samples

The samplesv0.4.0

The repository ships five samples under samples/. Four are implemented and hardware-verified: hello_inference (verified on the FRDM-MCXN947 in Phase 1 and re-verified through the Phase 2 scheduler on 2026-07-12), face_detection (the Phase 2 vision-pipeline demo, QEMU-tested and verified on the board), dual_model (the Phase 3 dual-core demo, board-verified on 2026-07-14 and extended with the dual-core OTA demo in Phase 4), and ota_update (the Phase 4 OTA demo, board-verified on 2026-07-15). keyword_spotting remains an intentional skeleton: it builds, boots, and prints a banner, but its body is marked "Implementation pending" on the roadmap.

At a glance

SampleDemonstratesStatus
hello_inferenceFull runtime round-trip: init, model registry, scheduler-driven inference (syn_infer_run_sync()), DSP argmax, memory stats, profiling summary, shellImplemented; hardware-verified on both the Phase 1 direct-HAL path and the Phase 2 scheduler path
face_detectionContinuous vision pipeline: resize → normalize → quantize → model → decode → NMS, with per-frame profilingImplemented and hardware-verified (Phase 2) — synthetic frames; camera/LCD bring-up deferred
keyword_spottingAudio keyword detection pipeline (planned)Skeleton — implementation pending
dual_modelAsymmetric dual-core: CPU0 serves the AI runtime, CPU1 alternates two models over IPCImplemented and hardware-verified (Phase 3) — stub-NPU model path
ota_updateEnd-to-end OTA model update over UART: factory install, streamed .synm update, activate, hot reload, rollbackImplemented and hardware-verified (Phase 4) — stub-NPU model path

hello_inference

The reference sample and the one used for all Phase 1 verification. As of Phase 2 its inference step runs through the scheduler rather than the raw HAL, so the profiler now captures real per-stage data. main() walks the API surface in order:

  1. Initialize the runtime with syn_init()
  2. Register a model (test_classify v1.0.0, INT8 in/out) in the registry
  3. Load the model binary via syn_hal_npu_load_model()
  4. Allocate a 1×16×16×3 INT8 input tensor (768 bytes) from the arena with syn_mem_tensor_alloc()
  5. Fill it with a gradient test pattern
  6. Run inference through the scheduler with syn_infer_run_sync(), timed with the cycle counter — the profiling marks fire at the stage boundaries, so syn prof last has real data afterwards
  7. Find the top prediction with syn_hal_dsp_argmax()
  8. Print memory statistics (syn_mem_print_stats())
  9. Print the profiling summary (syn_prof_print_summary()), then keep the shell alive

The 16×16×3 input size is chosen deliberately: it fits both the QEMU NPU stub (1 KB max input) and the FRDM hardware path. The Phase 1 direct-HAL path was verified at 1038 us end-to-end on the FRDM board versus 781 us on QEMU. On QEMU, the Phase 2 scheduler path (syn_infer_run_sync()) measures 1361 us wall against the same 781 us direct-HAL baseline — icount timing, stub NPU backend; the prediction is identical (class 0, confidence 127). On the FRDM board (v0.2.0, 2026-07-12) the scheduler path measures 1130 us wall against the 1038 us Phase 1 direct-HAL capture: about 92 us for the full scheduler path, of which the profiled dispatch overhead is only ~1 us (total 1069 us vs 1068 us NPU) — the rest is job submission, the scheduler thread, and completion signaling outside the marked region. All NPU timings are the deterministic stub backend.

shell
# FRDM-MCXN947 (with shell enabled)
west build -b frdm_mcxn947/mcxn947/cpu0 synaptic-os/samples/hello_inference --pristine
# QEMU (boards/qemu_cortex_m3.conf shrinks the arena and disables the shell)
west build -b qemu_cortex_m3 synaptic-os/samples/hello_inference --pristine
west build -t run
Note · Dummy model

The "model" is still a 64-byte zero-filled placeholder and the NPU backend produces a deterministic stub result. The point of the sample is exercising the full API path — real TFLite model execution on the Neutron NPU is still pending.

face_detection

Implemented in Phase 2: a continuous vision-pipeline demo that runs 30 frames through the full production path. Each frame is a deterministic synthetic 24×24×3 image (a bright blob moving across a gradient background, standing in for the OV7670 camera), pushed through resize (bilinear, to 12×12) → normalize (per-channel mean/std) → quantize (int8) → detector model (NPU HAL) → grid decode → non-maximum suppression, with the detections and per-frame profiling reported on the console. syn_mem_reset_ephemeral() runs after every frame, so the arena footprint stays constant across frames — confirmed on the board: peak 2784 bytes per frame, returning to 0 after every frame, 120 allocations and 30 ephemeral resets over the full run.

Measured on the FRDM-MCXN947 (v0.2.0, 2026-07-12; Cortex-M33 @ 150 MHz with hardware FPU, stub NPU backend): 4632 us per frame average — 215.8 FPS over 30 frames, with the last-frame profile splitting into 3562 us preprocess, 1040 us stub NPU, and 1 us postprocess. Measured on QEMU (icount, stub NPU, soft-float): 31.1 ms per frame average, 32.1 FPS effective — the M33's FPU cuts the float-heavy preprocessing roughly 8×. Preprocessing dominates the hardware frame time; with real Neutron kernels it becomes the bottleneck to attack (PowerQuad batch operations remain on the backlog). The final summary prints average frame time, FPS, and the profiling breakdown of the last inference.

shell
# QEMU (boards/qemu_cortex_m3.conf shrinks the arena and disables the shell)
west build -b qemu_cortex_m3 synaptic-os/samples/face_detection --pristine
west build -t run
# FRDM-MCXN947 (with shell enabled)
west build -b frdm_mcxn947/mcxn947/cpu0 synaptic-os/samples/face_detection --pristine
Note · Camera and LCD deferred

The frame source is synthetic and results go to the console: OV7670 (DVP) camera capture and LCD-PAR-S035 rendering are hardware bring-up steps deferred past Phase 2 (tracked). The pipeline, scheduler, and post-processing are unchanged by them. On the stub NPU the detections derive deterministically from a hash of the input; on Neutron hardware the model's real detections appear instead.

keyword_spotting

Placeholder for an audio keyword-detection demo. Its dependencies have arrived in Phase 2 — syn_preprocess_audio_mfcc and syn_hal_dsp_fft_f32() are implemented — but the sample body itself is still pending.

shell
west build -b frdm_mcxn947/mcxn947/cpu0 synaptic-os/samples/keyword_spotting --pristine

dual_model

Implemented in Phase 3: the asymmetric dual-core demo. CPU0 boots the full AI runtime (both models registered, shell, cross-core serving), verifies flash bank 1 holds an image, and releases CPU1; CPU1 runs a minimal application image that alternates cross-core inference requests — face_detect (96×96×3 INT8 frame, REALTIME priority) and keyword_spot (49×10 INT8 MFCC window, NORMAL priority) — at a 20 Hz sensor cadence, staging tensors zero-copy in the shared exchange slot. Only CPU0 has a console; syn ipc status shows the link, serve counters, and CPU1-measured round-trip statistics (see the syn shell).

Measured on the FRDM-MCXN947 (v0.3.0, 2026-07-14): CPU1 ready 1,514 µs after release, handshake at 2,554 µs, IPC round-trip 15 µs typical / 81 µs worst-case, and a 1,913-serve soak with zero errors (average serve 2,290 µs — stub-NPU baseline; real compiled Neutron models arrive in a later phase). If CPU1's flash bank is blank, CPU0 logs it and continues single-core with the full shell.

Two separate images: CPU0 uses the stock Zephyr board target, CPU1 uses the out-of-tree board port in boards/nxp/frdm_mcxn947_cpu1 (Zephyr 3.7 has no in-tree cpu1 target):

shell
# CPU0 (AI runtime + shell), flash bank 0
west build -b frdm_mcxn947/mcxn947/cpu0 synaptic-os/samples/dual_model --pristine -d build-dm-cpu0
# CPU1 (application), flash bank 1
west build -b frdm_mcxn947_cpu1/mcxn947/cpu1 synaptic-os/samples/dual_model/remote --pristine -d build-dm-cpu1
Note · Known limits, stated honestly

One cross-core request is in flight at a time (CPU1 callers serialize on a mutex); MPU write-protection is enforced on CPU0's side only, because CPU1 has no MPU; and the model path is the deterministic stub NPU — the latencies above bracket the runtime and transport, not silicon inference throughput.

ota_update

Implemented in Phase 4: the end-to-end OTA demo. On first boot with a blank store it installs a factory stub model into slot 0, then serves an inference tick every 3 seconds, printing which model and slot answered. From the host, tools/syn_ota_send.py streams a .synm image built by tools/syn_model_pack.py over the serial port using the syn ota shell transport (ack-paced 1024-byte hex lines; the sample raises the shell RX ring to 1 KB and the command buffer to 2320 bytes for exactly this). After syn ota activate the very next tick serves the updated model from the other slot; syn ota rollback returns to the previous one. Persistence comes free from the store: reboot mid-anything and the previous active model boots.

Measured on the FRDM-MCXN947 (v0.4.0, 2026-07-15): registry commit 1.9–2.6 ms and boot scan 23–189 µs against the 50 ms criterion; a slot-maximum 442,432-byte update staged, CRC-verified from flash, and activated in 80.5 s at 5.4 KB/s — transport-bound by ack-paced hex over 115200-baud UART (line-rate ceiling ≈5.6 KB/s), reported as measured; power loss mid-transfer left the factory model intact and serving; a staged update survived reboot and activated from IDLE. On QEMU the same demo runs against the RAM-emulated flash port. All inference is the deterministic stub NPU.

shell
# FRDM-MCXN947
west build -b frdm_mcxn947/mcxn947/cpu0 synaptic-os/samples/ota_update --pristine
# QEMU (RAM-backed flash port)
west build -b qemu_cortex_m3 synaptic-os/samples/ota_update --pristine
west build -t run
# then, from the host, stream an update over the serial port:
python3 synaptic-os/tools/syn_model_pack.py --input model.bin --name demo_model --output demo.synm
python3 synaptic-os/tools/syn_ota_send.py demo.synm --port /dev/ttyACM0
Note · Dual-core OTA

On a board flashed with dual_model on both cores, the same transport drives the dual-core OTA path: syn ota begin parks CPU1 (its XIP bank hosts the model slots) and offload pauses while local CPU0 inference keeps serving; activation releases CPU1 with a normal 1,514 µs boot + 1,519 µs handshake. Captured live in the Phase 4 OTA session.