neuralSPOT Integration
This guide explains how to integrate a HeliaAOT-generated module into a neuralSPOT-based application. NeuralSPOT is a full-featured, open-source AI SDK and toolkit optimized for Ambiq’s Apollo family of ultra-low-power SoCs. Once an AI model has been developed, neuralSPOT enables deployment of the model, suitable for linking into larger embedded applications.
1) Prerequisites
Hardware
- Ambiq EVB: at least one of Apollo4 Plus, Apollo4 Blue Plus, Apollo4 Lite, Apollo4 Blue Lite, Apollo3 Blue Plus, or Ambiq's flagship, the Apollo510.
Software
- Segger J-Link 8.12+
- Compilers: at least one of Arm GNU Toolchain 10.3+ or Armclang
- GNU Make
- Python 3.11+
Your application should include neuralSPOT as a submodule and keep third-party modules under <project-root>/modules/.
At minimum:
<project-root>/
├── modules/
│ ├── neuralspot/ # neuralSPOT SDK (git submodule)
│ └── ns-cmsis-nn/ # ns-cmsis-nn (submodule or copied)
└── ... # your application sources
Add neuralSPOT as a submodule
From your project root:
git submodule add git@github.com:AmbiqAI/neuralSPOT.git modules/neuralspot
git submodule update --init --recursive
Add ns-cmsis-nn
HeliaAOT-generated models rely on CMSIS-NN kernels provided by ns-cmsis-nn (rather than Tensorflow), which must be available at build time.
Expected layout after adding both neuralSPOT and ns-cmsis-nn:
2) Generate a neuralSPOT module with HeliaAOT
Run convert with module type neuralspot:
helia-aot convert \
--model.path <path-to-model.tflite> \
--module.path ./out \
--module.name helia_aot_nn \
--module.type neuralspot
Expected output layout
This command produces:
The generated module.mk is tailored for neuralSPOT and is used by the build system when the module is listed in your app Makefile.
3) Copy the HeliaAOT module into your app
Drop the generated module into your project:
4) Update your app Makefile
Your application Makefile must include both modules:
Drop-in Makefile (project name: main)
Place this Makefile at the root of your project.
include modules/neuralspot/make/local_overrides.mk
include modules/neuralspot/make/helpers.mk
include modules/neuralspot/make/neuralspot_config.mk
include modules/neuralspot/make/neuralspot_toolchain.mk
include modules/neuralspot/make/jlink.mk
ifeq ($(TOOLCHAIN),arm)
COMPDIR := armclang
else ifeq ($(TOOLCHAIN),arm-none-eabi)
COMPDIR := gcc
endif
libraries :=
override_libraries :=
lib_prebuilt :=
sources :=
includes_api :=
local_app_name := main
# neuralSPOT internal modules (paths relative to modules/neuralspot/)
ns_modules := neuralspot/ns-core
ns_modules += neuralspot/ns-harness
ns_modules += neuralspot/ns-peripherals
ns_modules += neuralspot/ns-utils
ns_modules += neuralspot/ns-uart
ns_modules += neuralspot/ns-rpc
ns_modules += neuralspot/ns-ipc
ifeq ($(USB_PRESENT),1)
ns_modules += neuralspot/ns-usb
endif
# Autodeploy doesn't need the full set of modules
ifneq ($(AUTODEPLOY),1)
ns_modules += neuralspot/ns-i2c
ns_modules += neuralspot/ns-audio
ifneq ($(ARCH),apollo3)
ns_modules += neuralspot/ns-spi
ns_modules += neuralspot/ns-camera
ifneq ($(PLATFORM),apollo510L_eb)
ns_modules += neuralspot/ns-imu
endif
endif
ns_modules += neuralspot/ns-nnsp
ns_modules += neuralspot/ns-features
ifeq ($(BLE_SUPPORTED),1)
ns_modules += neuralspot/ns-ble
endif
endif
# neuralSPOT extern modules (paths relative to modules/neuralspot/)
ns_modules += extern/AmbiqSuite/$(AS_VERSION)
ns_modules += extern/CMSIS/$(CMSIS_DSP_VERSION)
ns_modules += extern/tensorflow/$(TF_VERSION)
ns_modules += extern/codecs/opus-precomp
ns_modules += extern/codecs/octopus
ns_modules += extern/drivers/tdk/icm45605
ifeq ($(BLE_SUPPORTED),1)
ifeq ($(PLATFORM),apollo330mP_evb)
ns_modules += extern/AmbiqSuite/$(AS_VERSION)/third_party/open-amp
endif
ns_modules += extern/AmbiqSuite/$(AS_VERSION)/third_party/cordio
endif
ns_modules += extern/erpc/$(ERPC_VERSION)
# neuralSPOT add-on modules
local_modules := modules/ns-cmsis-nn
local_modules += modules/helia_aot_nn
TARGET = $(local_app_name)
sources := $(wildcard src/*.c)
sources += $(wildcard src/*.cc)
sources += $(wildcard src/*.cpp)
sources += $(wildcard src/*.s)
targets := $(BINDIR)/$(local_app_name).axf
targets += $(BINDIR)/$(local_app_name).bin
objects = $(call source-to-object,$(sources))
dependencies = $(subst .o,.d,$(objects))
CFLAGS += $(addprefix -D,$(DEFINES))
CFLAGS += $(addprefix -I ,$(includes_api)) # needed for modules
CFLAGS += -O0
ifeq ($(BOARD),apollo510)
LINKER_EXT := _sbl
else ifeq ($(BOARD),apollo4p)
LINKER_EXT :=
endif
ifeq ($(TOOLCHAIN),arm)
LINKER_FILE := modules/neuralspot/neuralspot/ns-core/src/$(BOARD)/$(COMPDIR)/linker_script$(LINKER_EXT).sct
else ifeq ($(TOOLCHAIN),arm-none-eabi)
LINKER_FILE := modules/neuralspot/neuralspot/ns-core/src/$(BOARD)/$(COMPDIR)/linker_script$(LINKER_EXT).ld
endif
.PHONY: all
all:
include $(addsuffix /module.mk,$(addprefix modules/neuralspot/,$(ns_modules)) $(local_modules))
all: $(BINDIR) $(libraries) $(override_libraries) $(objects) $(targets)
.PHONY: clean
clean:
ifeq ($(OS),Windows_NT)
@echo "Windows_NT"
@echo $(Q) $(RM) -rf $(BINDIR)/*
$(Q) $(RM) -rf $(BINDIR)/*
else
$(Q) $(RM) -rf $(BINDIR) $(JLINK_CF)
endif
ifneq "$(MAKECMDGOALS)" "clean"
include $(dependencies)
endif
$(BINDIR):
$(Q) $(MKD) -p $@
$(BINDIR)/%.o: %.cc
@echo " Compiling $(COMPILERNAME) $< to make $@"
$(Q) $(MKD) -p $(@D)
$(Q) $(CC) -c $(CFLAGS) $(CCFLAGS) $< -o $@
$(BINDIR)/%.o: %.cpp
@echo " Compiling $(COMPILERNAME) $< to make $@"
$(Q) $(MKD) -p $(@D)
$(Q) $(CC) -c $(CFLAGS) $(CCFLAGS) $< -o $@
$(BINDIR)/%.o: %.c
@echo " Compiling $(COMPILERNAME) $< to make $@"
$(Q) $(MKD) -p $(@D)
$(Q) $(CC) -c $(CFLAGS) $(CONLY_FLAGS) $< -o $@
$(BINDIR)/%.o: %.s
@echo " Assembling $(COMPILERNAME) $<"
$(Q) $(MKD) -p $(@D)
$(Q) $(CC) -c $(ASMFLAGS) $< -o $@
$(BINDIR)/$(local_app_name).axf: $(objects) $(libraries) $(lib_prebuilt) $(override_libraries)
@echo " Linking $(COMPILERNAME) $@"
$(Q) $(MKD) -p $(@D)
ifeq ($(TOOLCHAIN),arm)
$(Q) $(LD) $^ $(LFLAGS) --list=$*.map -o $@
else
$(Q) $(CC) -Wl,-T,$(LINKER_FILE) -o $@ $^ $(LFLAGS)
endif
ifeq ($(TOOLCHAIN),arm)
$(BINDIR)/$(local_app_name).bin: $(BINDIR)/$(local_app_name).axf
@echo " Copying $(COMPILERNAME) $@..."
$(Q) $(MKD) -p $(@D)
$(Q) $(CP) $(CPFLAGS) $@ $<
$(Q) $(OD) $(ODFLAGS) $< --output $*.txt
else
$(BINDIR)/$(local_app_name).bin: $(BINDIR)/$(local_app_name).axf
@echo " Copying $(COMPILERNAME) $@..."
$(Q) $(MKD) -p $(@D)
$(Q) $(CP) $(CPFLAGS) $< $@
$(Q) $(OD) $(ODFLAGS) $< > $(@:.bin=.lst)
endif
$(JLINK_CF):
@echo " Creating JLink command sequence input file..."
$(Q) echo "ExitOnError 1" > $@
$(Q) echo "Reset" >> $@
$(Q) echo "LoadFile $(BINDIR)/$(TARGET).bin, $(JLINK_PF_ADDR)" >> $@
$(Q) echo "Exit" >> $@
.PHONY: deploy
deploy: $(JLINK_CF)
@echo " Deploying $< to device (ensure JLink USB connected and powered on)..."
$(Q) $(JLINK) $(JLINK_CMD)
# $(Q) $(RM) $(JLINK_CF)
.PHONY: view
view:
@echo " Printing SWO output (ensure JLink USB connected and powered on)..."
$(Q) $(JLINK_SWO) $(JLINK_SWO_CMD)
# $(Q) $(RM) $(JLINK_CF)
%.d: ;
Key points in this example Makefile:
- The application name is
main(local_app_name := main), so the build outputsmain.axfandmain.bin. - The neuralSPOT internal modules list is the SDK core for running firmware on Ambiq EVBs.
- The add-on modules include
modules/ns-cmsis-nnand the generatedmodules/helia_aot_nnmodule. include $(addsuffix /module.mk,$(modules))pulls in each module’s build rules (including the HeliaAOT-generatedmodule.mk).- You can switch Ambiq boards by uncommenting the desired
PLATFORMand matchingAS_VERSIONinmodules/neuralspot/make/local_overrides.mk.
5) Minimal runtime sequence (main.c)
#include "aot_model.h"
#include "aot_test_case.h" // optional
static void model_layer_status_cb(
int32_t op,
aot_operator_state_t state,
int32_t status,
void *user_data
) {
if (state == aot_op_state_init_finished) {
AOT_PRINTF("Operator %d init finished with status %d\n", op, status);
}
if (state == aot_op_state_run_finished) {
AOT_PRINTF("Operator %d run finished with status %d\n", op, status);
}
}
static aot_model_context_t aot_model_ctx = {
.callback = model_layer_status_cb,
.user_data = NULL
};
void
hardware_init(void) {
ns_core_config_t nsCoreCfg = {
.api = &ns_core_V1_0_0
};
NS_TRY(ns_core_init(&nsCoreCfg), "Core Init failed.\b");
NS_TRY(ns_power_config(&ns_development_default), "Power Init Failed\n");
ns_itm_printf_enable();
ns_interrupt_master_enable();
ns_set_performance_mode(NS_MAXIMUM_PERF);
}
int main(void) {
int32_t status;
// 0. Hardware init
hardware_init();
// 1. Initialize model
status = aot_model_init(&aot_model_ctx);
// 2. Copy input tensors to context
for (int i = 0; i < aot_num_inputs; i++) {
arm_memset_s8(
aot_model_ctx.inputs[i].data,
0,
aot_model_ctx.inputs[i].size
);
}
// 3. Invoke model
status = aot_model_run(&aot_model_ctx);
// 4. Retrieve output tensors from context
// ...
// Alternatively, run included smoke test
aot_test_case_init();
status = aot_test_case_run();
return 0;
}
6) Build, flash, and view output
From your project root: