Skip to content

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.

cp -r <path-to-ns-cmsis-nn> <project-root>/modules/ns-cmsis-nn
git submodule add https://github.com/AmbiqAI/ns-cmsis-nn.git modules/ns-cmsis-nn
git submodule update --init --recursive

Expected layout after adding both neuralSPOT and ns-cmsis-nn:

<project-root>/
├── modules/
│   ├── neuralspot/
│   └── 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:

out/helia_aot_nn/
├── includes-api/
├── src/
├── module.mk
├── LICENSE
└── README.md

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:

cp -r ./out/helia_aot_nn <project-root>/modules/helia_aot_nn

4) Update your app Makefile

Your application Makefile must include both modules:

modules      += $(PROJECT_ROOT)/modules/ns-cmsis-nn
modules      += $(PROJECT_ROOT)/modules/helia_aot_nn

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 outputs main.axf and main.bin.
  • The neuralSPOT internal modules list is the SDK core for running firmware on Ambiq EVBs.
  • The add-on modules include modules/ns-cmsis-nn and the generated modules/helia_aot_nn module.
  • include $(addsuffix /module.mk,$(modules)) pulls in each module’s build rules (including the HeliaAOT-generated module.mk).
  • You can switch Ambiq boards by uncommenting the desired PLATFORM and matching AS_VERSION in modules/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:

make clean
make -j        # builds everything
make deploy    # flashes the default example to the EVB
make view      # connects to the J-Link SWO interface for printf output