diff --git a/CMakeLists.txt b/CMakeLists.txt index 257ce57..1803ac4 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,15 +15,16 @@ project(embedDIP DESCRIPTION "Portable embedded digital image processing library" ) -set(EMBEDDIP_TARGET_BOARD "" CACHE STRING "Target board (required): STM32F7, STM32H7S, ESP32, or HOST") -set_property(CACHE EMBEDDIP_TARGET_BOARD PROPERTY STRINGS "STM32F7" "STM32H7S" "ESP32" "HOST") +set(EMBEDDIP_TARGET_BOARD "" CACHE STRING "Target board (required): STM32F7, STM32H7S, STM32N6, ESP32, or HOST") +set_property(CACHE EMBEDDIP_TARGET_BOARD PROPERTY STRINGS "STM32F7" "STM32H7S" "STM32N6" "ESP32" "HOST") set(EMBEDDIP_ARCH "" CACHE STRING "Architecture family (required): ARM, XTENSA, or HOST") set_property(CACHE EMBEDDIP_ARCH PROPERTY STRINGS "ARM" "XTENSA" "HOST") -set(EMBEDDIP_CPU "" CACHE STRING "CPU variant (required): CORTEX_M7, LX6, LX7, NATIVE") -set_property(CACHE EMBEDDIP_CPU PROPERTY STRINGS "CORTEX_M7" "LX6" "LX7" "NATIVE") +set(EMBEDDIP_CPU "" CACHE STRING "CPU variant (required): CORTEX_M7, CORTEX_M55, LX6, LX7, NATIVE") +set_property(CACHE EMBEDDIP_CPU PROPERTY STRINGS "CORTEX_M7" "CORTEX_M55" "LX6" "LX7" "NATIVE") +set(EMBEDDIP_STM32CUBE_N6_ROOT "" CACHE PATH "Path to the STM32CubeN6 SDK root") set(EMBEDDIP_STM32CUBE_H7RS_ROOT "" CACHE PATH "Path to the STM32CubeH7RS SDK root") option(EMBEDDIP_ENABLE_IMAGE_PROCESSING "Enable image processing modules" ON) @@ -32,7 +33,7 @@ option(EMBEDDIP_ENABLE_DISPLAY_OUTPUT "Enable display output interfaces" ON) option(EMBEDDIP_BUILD_TESTS "Build CTest safety-net tests" OFF) if(EMBEDDIP_TARGET_BOARD STREQUAL "") - message(FATAL_ERROR "EMBEDDIP_TARGET_BOARD is required. Supported values: STM32F7, STM32H7S, ESP32, HOST") + message(FATAL_ERROR "EMBEDDIP_TARGET_BOARD is required. Supported values: STM32F7, STM32H7S, STM32N6, ESP32, HOST") endif() if(EMBEDDIP_ARCH STREQUAL "") @@ -40,7 +41,7 @@ if(EMBEDDIP_ARCH STREQUAL "") endif() if(EMBEDDIP_CPU STREQUAL "") - message(FATAL_ERROR "EMBEDDIP_CPU is required. Supported values: CORTEX_M7, LX6, LX7, NATIVE") + message(FATAL_ERROR "EMBEDDIP_CPU is required. Supported values: CORTEX_M7, CORTEX_M55, LX6, LX7, NATIVE") endif() # Explicit compatibility matrix between board, architecture family and CPU @@ -57,6 +58,10 @@ elseif(EMBEDDIP_TARGET_BOARD STREQUAL "ESP32") if(EMBEDDIP_ARCH STREQUAL "XTENSA" AND (EMBEDDIP_CPU STREQUAL "LX6" OR EMBEDDIP_CPU STREQUAL "LX7")) set(_embeddip_pair_valid TRUE) endif() +elseif(EMBEDDIP_TARGET_BOARD STREQUAL "STM32N6") + if(EMBEDDIP_ARCH STREQUAL "ARM" AND EMBEDDIP_CPU STREQUAL "CORTEX_M55") + set(_embeddip_pair_valid TRUE) + endif() elseif(EMBEDDIP_TARGET_BOARD STREQUAL "HOST") if(EMBEDDIP_ARCH STREQUAL "HOST" AND EMBEDDIP_CPU STREQUAL "NATIVE") set(_embeddip_pair_valid TRUE) @@ -66,7 +71,7 @@ endif() if(NOT _embeddip_pair_valid) message(FATAL_ERROR "Invalid board/arch/cpu combination: ${EMBEDDIP_TARGET_BOARD} + ${EMBEDDIP_ARCH} + ${EMBEDDIP_CPU}. " - "Supported: STM32F7+ARM+CORTEX_M7, STM32H7S+ARM+CORTEX_M7, ESP32+XTENSA+LX6, ESP32+XTENSA+LX7, HOST+HOST+NATIVE" + "Supported: STM32F7+ARM+CORTEX_M7, STM32H7S+ARM+CORTEX_M7, STM32N6+ARM+CORTEX_M55, ESP32+XTENSA+LX6, ESP32+XTENSA+LX7, HOST+HOST+NATIVE" ) endif() @@ -83,6 +88,15 @@ set(CORE_SOURCES core/image.h ) +set(RUNTIME_SOURCES + runtime/runtime.c + runtime/runtime.h + runtime/model_manifest.c + runtime/model_manifest.h + runtime/stedgeai_n6/backend.c + runtime/stedgeai_n6/backend.h +) + set(IMGPROC_SOURCES # Main header (includes all sub-modules) imgproc/pixel.h @@ -184,6 +198,7 @@ include("${EMBEDDIP_ARCH_PROFILE_FILE}") # === Create Library Target === add_library(embedDIP STATIC ${CORE_SOURCES} + ${RUNTIME_SOURCES} ${IMGPROC_SOURCES} ${EMBEDDIP_BOARD_SOURCES} ${EMBEDDIP_ARCH_SOURCES} @@ -295,6 +310,7 @@ install(FILES install(DIRECTORY core/ + runtime/ imgproc/ device/ board/ diff --git a/board/stm32n6/board_profile.cmake b/board/stm32n6/board_profile.cmake new file mode 100644 index 0000000..c1fae25 --- /dev/null +++ b/board/stm32n6/board_profile.cmake @@ -0,0 +1,27 @@ +# Board profile: STM32N6570-DK + +if(NOT IS_DIRECTORY "${EMBEDDIP_STM32CUBE_N6_ROOT}") + message(FATAL_ERROR + "EMBEDDIP_STM32CUBE_N6_ROOT must name an existing STM32CubeN6 SDK directory: " + "'${EMBEDDIP_STM32CUBE_N6_ROOT}'") +endif() + +set(EMBEDDIP_BOARD_SOURCES + ${BOARD_COMMON_SOURCES} + board/stm32n6/board_stm32n6_memory.c +) + +set(EMBEDDIP_DEVICE_SOURCES) + +set(EMBEDDIP_BOARD_DEFINES + EMBED_DIP_BOARD_STM32N6=1 + STM32N6xx + STM32N657xx +) + +set(EMBEDDIP_BOARD_INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/board/stm32n6 + ${EMBEDDIP_STM32CUBE_N6_ROOT}/Drivers/CMSIS/Device/ST/STM32N6xx/Include + ${EMBEDDIP_STM32CUBE_N6_ROOT}/Drivers/CMSIS/Core/Include + ${EMBEDDIP_STM32CUBE_N6_ROOT}/Drivers/CMSIS/DSP/Include +) diff --git a/board/stm32n6/board_stm32n6_memory.c b/board/stm32n6/board_stm32n6_memory.c new file mode 100644 index 0000000..0137b25 --- /dev/null +++ b/board/stm32n6/board_stm32n6_memory.c @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 EmbedDIP + +#include "core/memory_manager.h" + +#include +#include +#include + +#include + +#include + +typedef struct { + uintptr_t start; + uintptr_t end; + uintptr_t cursor; +} n6_memory_range_t; + +static n6_memory_range_t fast_sram; +static n6_memory_range_t dma_memory; +static n6_memory_range_t psram; +static int memory_initialized; + +static void n6_reset_range(n6_memory_range_t *range, uint8_t *start, uint8_t *end) +{ + range->start = (uintptr_t)start; + range->end = (uintptr_t)end; + range->cursor = range->start; +} + +void memory_init(uintptr_t pool_start_addr) +{ + (void)pool_start_addr; + n6_reset_range(&fast_sram, __embeddip_fast_sram_start__, __embeddip_fast_sram_end__); + n6_reset_range(&dma_memory, __embeddip_dma_start__, __embeddip_dma_end__); + n6_reset_range(&psram, __embeddip_psram_start__, __embeddip_psram_end__); + memory_initialized = 1; +} + +static int n6_alignment_is_power_of_two(size_t alignment) +{ + return alignment != 0u && (alignment & (alignment - 1u)) == 0u; +} + +static n6_memory_range_t *n6_range_for_region(embeddip_memory_region_t region) +{ + switch (region) { + case EMBEDDIP_MEMORY_REGION_DEFAULT: + case EMBEDDIP_MEMORY_REGION_FAST_SRAM: + return &fast_sram; + case EMBEDDIP_MEMORY_REGION_DMA: + return &dma_memory; + case EMBEDDIP_MEMORY_REGION_PSRAM: + return &psram; + case EMBEDDIP_MEMORY_REGION_EXTERNAL_FLASH: + default: + return NULL; + } +} + +static embeddip_status_t n6_allocate_region(embeddip_memory_region_t region, + size_t size, + size_t alignment, + void **allocation) +{ + n6_memory_range_t *range; + uintptr_t aligned_cursor; + uintptr_t alignment_mask; + + if (allocation == NULL) { + return EMBEDDIP_ERROR_NULL_PTR; + } + *allocation = NULL; + + if (!n6_alignment_is_power_of_two(alignment)) { + return EMBEDDIP_ERROR_INVALID_ARG; + } + if (size == 0u) { + return EMBEDDIP_ERROR_INVALID_SIZE; + } + if (region == EMBEDDIP_MEMORY_REGION_EXTERNAL_FLASH) { + return EMBEDDIP_ERROR_NOT_SUPPORTED; + } + + range = n6_range_for_region(region); + if (range == NULL) { + return EMBEDDIP_ERROR_INVALID_ARG; + } + if (!memory_initialized) { + memory_init(0u); + range = n6_range_for_region(region); + } + + alignment_mask = (uintptr_t)alignment - 1u; + if (range->end < range->start || range->cursor > UINTPTR_MAX - alignment_mask) { + return EMBEDDIP_ERROR_OUT_OF_MEMORY; + } + aligned_cursor = (range->cursor + alignment_mask) & ~alignment_mask; + if (aligned_cursor > range->end || size > range->end - aligned_cursor) { + return EMBEDDIP_ERROR_OUT_OF_MEMORY; + } + + range->cursor = aligned_cursor + size; + *allocation = (void *)aligned_cursor; + return EMBEDDIP_OK; +} + +void *embeddip_board_alloc_region(embeddip_memory_region_t region, size_t size, size_t alignment) +{ + void *allocation; + + if (n6_allocate_region(region, size, alignment, &allocation) != EMBEDDIP_OK) { + return NULL; + } + return allocation; +} + +void *memory_alloc(size_t size) +{ + return embeddip_board_alloc_region(EMBEDDIP_MEMORY_REGION_DEFAULT, size, alignof(max_align_t)); +} + +void memory_free(void *ptr) +{ + (void)ptr; +} + +void *memory_realloc(void *ptr, size_t new_size) +{ + if (ptr == NULL) { + return memory_alloc(new_size); + } + return NULL; +} + +static embeddip_status_t +n6_cache_span(const void *address, size_t size, uintptr_t *rounded_start, int32_t *rounded_size) +{ + const uintptr_t line_mask = (uintptr_t)EMBEDDIP_N6_CACHE_LINE_BYTES - 1u; + uintptr_t start; + uintptr_t end; + uintptr_t rounded_end; + uintptr_t span; + + if (address == NULL) { + return EMBEDDIP_ERROR_NULL_PTR; + } + if (size == 0u) { + return EMBEDDIP_ERROR_INVALID_SIZE; + } + + start = (uintptr_t)address; + if (size > UINTPTR_MAX - start) { + return EMBEDDIP_ERROR_OVERFLOW; + } + end = start + size; + if (end > UINTPTR_MAX - line_mask) { + return EMBEDDIP_ERROR_OVERFLOW; + } + + *rounded_start = start & ~line_mask; + rounded_end = (end + line_mask) & ~line_mask; + span = rounded_end - *rounded_start; + if (span > INT32_MAX) { + return EMBEDDIP_ERROR_OVERFLOW; + } + + *rounded_size = (int32_t)span; + return EMBEDDIP_OK; +} + +embeddip_status_t embeddip_board_cache_clean(const void *address, size_t size) +{ + uintptr_t rounded_start; + int32_t rounded_size; + embeddip_status_t status = n6_cache_span(address, size, &rounded_start, &rounded_size); + + if (status != EMBEDDIP_OK) { + return status; + } + SCB_CleanDCache_by_Addr((void *)rounded_start, rounded_size); + return EMBEDDIP_OK; +} + +embeddip_status_t embeddip_board_cache_invalidate(const void *address, size_t size) +{ + uintptr_t rounded_start; + int32_t rounded_size; + embeddip_status_t status = n6_cache_span(address, size, &rounded_start, &rounded_size); + + if (status != EMBEDDIP_OK) { + return status; + } + SCB_InvalidateDCache_by_Addr((void *)rounded_start, rounded_size); + return EMBEDDIP_OK; +} diff --git a/board/stm32n6/configs.h b/board/stm32n6/configs.h new file mode 100644 index 0000000..4ff2b51 --- /dev/null +++ b/board/stm32n6/configs.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 EmbedDIP + +#ifndef EMBEDDIP_STM32N6_CONFIGS_H +#define EMBEDDIP_STM32N6_CONFIGS_H + +#include + +#define EMBEDDIP_N6_CACHE_LINE_BYTES 32u + +#ifdef __cplusplus +extern "C" { +#endif + +extern uint8_t __embeddip_fast_sram_start__[]; +extern uint8_t __embeddip_fast_sram_end__[]; +extern uint8_t __embeddip_dma_start__[]; +extern uint8_t __embeddip_dma_end__[]; +extern uint8_t __embeddip_psram_start__[]; +extern uint8_t __embeddip_psram_end__[]; +extern uint8_t __embeddip_xspi_flash_start__[]; +extern uint8_t __embeddip_xspi_flash_end__[]; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/embedDIP_configs.h b/embedDIP_configs.h index f5b4817..eaf33b4 100755 --- a/embedDIP_configs.h +++ b/embedDIP_configs.h @@ -28,6 +28,7 @@ /* Uncomment only if you do not provide these from the build system. */ /* #define EMBED_DIP_BOARD_STM32F7 1 */ /* #define EMBED_DIP_BOARD_STM32H7S 1 */ +/* #define EMBED_DIP_BOARD_STM32N6 1 */ /* #define EMBED_DIP_BOARD_ESP32 1 */ /* #define EMBED_DIP_BOARD_HOST 1 */ @@ -35,10 +36,11 @@ /* #define EMBED_DIP_ARCH_XTENSA 1 */ /* #define EMBED_DIP_ARCH_HOST 1 */ -/* #define EMBED_DIP_CPU_CORTEX_M7 1 */ -/* #define EMBED_DIP_CPU_LX6 1 */ -/* #define EMBED_DIP_CPU_LX7 1 */ -/* #define EMBED_DIP_CPU_NATIVE 1 */ +/* #define EMBED_DIP_CPU_CORTEX_M7 1 */ +/* #define EMBED_DIP_CPU_CORTEX_M55 1 */ +/* #define EMBED_DIP_CPU_LX6 1 */ +/* #define EMBED_DIP_CPU_LX7 1 */ +/* #define EMBED_DIP_CPU_NATIVE 1 */ /* -------------------------------------------------------------------------- */ /* Arduino auto-detection (Library Manager friendly defaults) */ @@ -48,11 +50,13 @@ * infer them from Arduino core/platform macros so sketches can compile * without extra CLI flags. */ -#if !defined(EMBED_DIP_BOARD_STM32F7) && !defined(EMBED_DIP_BOARD_STM32H7S) && !defined(EMBED_DIP_BOARD_ESP32) && !defined(EMBED_DIP_BOARD_HOST) +#if !defined(EMBED_DIP_BOARD_STM32F7) && !defined(EMBED_DIP_BOARD_STM32H7S) && !defined(EMBED_DIP_BOARD_STM32N6) && !defined(EMBED_DIP_BOARD_ESP32) && !defined(EMBED_DIP_BOARD_HOST) #if defined(ARDUINO_ARCH_ESP32) #define EMBED_DIP_BOARD_ESP32 1 #elif defined(STM32H7S7xx) #define EMBED_DIP_BOARD_STM32H7S 1 + #elif defined(STM32N6xx) + #define EMBED_DIP_BOARD_STM32N6 1 #elif defined(STM32F7xx) #define EMBED_DIP_BOARD_STM32F7 1 #endif @@ -61,12 +65,12 @@ #if !defined(EMBED_DIP_ARCH_ARM) && !defined(EMBED_DIP_ARCH_XTENSA) && !defined(EMBED_DIP_ARCH_HOST) #if defined(EMBED_DIP_BOARD_ESP32) #define EMBED_DIP_ARCH_XTENSA 1 - #elif defined(EMBED_DIP_BOARD_STM32F7) || defined(EMBED_DIP_BOARD_STM32H7S) + #elif defined(EMBED_DIP_BOARD_STM32F7) || defined(EMBED_DIP_BOARD_STM32H7S) || defined(EMBED_DIP_BOARD_STM32N6) #define EMBED_DIP_ARCH_ARM 1 #endif #endif -#if !defined(EMBED_DIP_CPU_CORTEX_M7) && !defined(EMBED_DIP_CPU_LX6) && !defined(EMBED_DIP_CPU_LX7) && !defined(EMBED_DIP_CPU_NATIVE) +#if !defined(EMBED_DIP_CPU_CORTEX_M7) && !defined(EMBED_DIP_CPU_CORTEX_M55) && !defined(EMBED_DIP_CPU_LX6) && !defined(EMBED_DIP_CPU_LX7) && !defined(EMBED_DIP_CPU_NATIVE) #if defined(EMBED_DIP_BOARD_ESP32) /* * ESP32/ESP32-S2/ESP32-S3 families are LX6/LX7. Prefer explicit IDF @@ -79,14 +83,16 @@ #endif #elif defined(EMBED_DIP_BOARD_STM32F7) || defined(EMBED_DIP_BOARD_STM32H7S) #define EMBED_DIP_CPU_CORTEX_M7 1 + #elif defined(EMBED_DIP_BOARD_STM32N6) + #define EMBED_DIP_CPU_CORTEX_M55 1 #endif #endif /* Sanity check: exactly one board. */ -#if ((defined(EMBED_DIP_BOARD_STM32F7) ? 1 : 0) + (defined(EMBED_DIP_BOARD_STM32H7S) ? 1 : 0) + (defined(EMBED_DIP_BOARD_ESP32) ? 1 : 0) + (defined(EMBED_DIP_BOARD_HOST) ? 1 : 0)) == 0 +#if ((defined(EMBED_DIP_BOARD_STM32F7) ? 1 : 0) + (defined(EMBED_DIP_BOARD_STM32H7S) ? 1 : 0) + (defined(EMBED_DIP_BOARD_STM32N6) ? 1 : 0) + (defined(EMBED_DIP_BOARD_ESP32) ? 1 : 0) + (defined(EMBED_DIP_BOARD_HOST) ? 1 : 0)) == 0 #error \ - "No board selected: define exactly one of EMBED_DIP_BOARD_STM32F7, EMBED_DIP_BOARD_STM32H7S, EMBED_DIP_BOARD_ESP32, or EMBED_DIP_BOARD_HOST." -#elif ((defined(EMBED_DIP_BOARD_STM32F7) ? 1 : 0) + (defined(EMBED_DIP_BOARD_STM32H7S) ? 1 : 0) + (defined(EMBED_DIP_BOARD_ESP32) ? 1 : 0) + (defined(EMBED_DIP_BOARD_HOST) ? 1 : 0)) > 1 + "No board selected: define exactly one of EMBED_DIP_BOARD_STM32F7, EMBED_DIP_BOARD_STM32H7S, EMBED_DIP_BOARD_STM32N6, EMBED_DIP_BOARD_ESP32, or EMBED_DIP_BOARD_HOST." +#elif ((defined(EMBED_DIP_BOARD_STM32F7) ? 1 : 0) + (defined(EMBED_DIP_BOARD_STM32H7S) ? 1 : 0) + (defined(EMBED_DIP_BOARD_STM32N6) ? 1 : 0) + (defined(EMBED_DIP_BOARD_ESP32) ? 1 : 0) + (defined(EMBED_DIP_BOARD_HOST) ? 1 : 0)) > 1 #error \ "Multiple boards selected: define only one EMBED_DIP_BOARD_* macro." #endif @@ -100,11 +106,11 @@ #endif /* Sanity check: exactly one CPU variant. */ -#if ((defined(EMBED_DIP_CPU_CORTEX_M7) ? 1 : 0) + (defined(EMBED_DIP_CPU_LX6) ? 1 : 0) + \ +#if ((defined(EMBED_DIP_CPU_CORTEX_M7) ? 1 : 0) + (defined(EMBED_DIP_CPU_CORTEX_M55) ? 1 : 0) + (defined(EMBED_DIP_CPU_LX6) ? 1 : 0) + \ (defined(EMBED_DIP_CPU_LX7) ? 1 : 0) + (defined(EMBED_DIP_CPU_NATIVE) ? 1 : 0)) == 0 #error \ - "No CPU selected: define exactly one of EMBED_DIP_CPU_CORTEX_M7, EMBED_DIP_CPU_LX6, EMBED_DIP_CPU_LX7, or EMBED_DIP_CPU_NATIVE." -#elif ((defined(EMBED_DIP_CPU_CORTEX_M7) ? 1 : 0) + (defined(EMBED_DIP_CPU_LX6) ? 1 : 0) + \ + "No CPU selected: define exactly one of EMBED_DIP_CPU_CORTEX_M7, EMBED_DIP_CPU_CORTEX_M55, EMBED_DIP_CPU_LX6, EMBED_DIP_CPU_LX7, or EMBED_DIP_CPU_NATIVE." +#elif ((defined(EMBED_DIP_CPU_CORTEX_M7) ? 1 : 0) + (defined(EMBED_DIP_CPU_CORTEX_M55) ? 1 : 0) + (defined(EMBED_DIP_CPU_LX6) ? 1 : 0) + \ (defined(EMBED_DIP_CPU_LX7) ? 1 : 0) + (defined(EMBED_DIP_CPU_NATIVE) ? 1 : 0)) > 1 #error "Multiple CPUs selected: define only one EMBED_DIP_CPU_* macro." #endif @@ -120,6 +126,11 @@ #error \ "Invalid combination: EMBED_DIP_BOARD_STM32H7S requires EMBED_DIP_ARCH_ARM + EMBED_DIP_CPU_CORTEX_M7." #endif +#elif defined(EMBED_DIP_BOARD_STM32N6) + #if !(defined(EMBED_DIP_ARCH_ARM) && defined(EMBED_DIP_CPU_CORTEX_M55)) + #error \ + "Invalid combination: EMBED_DIP_BOARD_STM32N6 requires EMBED_DIP_ARCH_ARM + EMBED_DIP_CPU_CORTEX_M55." + #endif #elif defined(EMBED_DIP_BOARD_ESP32) #if !(defined(EMBED_DIP_ARCH_XTENSA) && \ (defined(EMBED_DIP_CPU_LX6) || defined(EMBED_DIP_CPU_LX7))) @@ -187,6 +198,22 @@ #define DEVICE_STM32H7S_UART 1 #endif +/* =============================== STM32N6 =================================== */ +#elif defined(EMBED_DIP_BOARD_STM32N6) + #ifndef STM32N6xx + #define STM32N6xx 1 + #endif + + #ifndef ENABLE_IMAGE_PROCESSING + #define ENABLE_IMAGE_PROCESSING 1 + #endif + #ifndef ENABLE_CAMERA_INPUT + #define ENABLE_CAMERA_INPUT 0 + #endif + #ifndef ENABLE_DISPLAY_OUTPUT + #define ENABLE_DISPLAY_OUTPUT 0 + #endif + /* =============================== ESP32 ==================================== */ #elif defined(EMBED_DIP_BOARD_ESP32) #ifndef ARDUINO_ARCH_ESP32 diff --git a/runtime/model_manifest.c b/runtime/model_manifest.c new file mode 100644 index 0000000..b673a13 --- /dev/null +++ b/runtime/model_manifest.c @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 EmbedDIP + +#include "runtime/model_manifest.h" + +#include + +static int string_is_nonempty(const char *value) +{ + return value != NULL && value[0] != '\0'; +} + +static embeddip_status_t tensor_contract_validate(const cv_tensor_t *tensor) +{ + uint64_t element_bytes; + uint64_t expected_bytes; + + if (tensor->bytes == 0u || tensor->width == 0u || tensor->height == 0u || + tensor->channels == 0u) { + return EMBEDDIP_ERROR_INVALID_SIZE; + } + if (tensor->type != CV_TENSOR_U8 && tensor->type != CV_TENSOR_I8 && + tensor->type != CV_TENSOR_F32) { + return EMBEDDIP_ERROR_NOT_SUPPORTED; + } + if (tensor->layout != CV_TENSOR_HWC && tensor->layout != CV_TENSOR_CHW) { + return EMBEDDIP_ERROR_NOT_SUPPORTED; + } + element_bytes = tensor->type == CV_TENSOR_F32 ? 4u : 1u; + expected_bytes = (uint64_t)tensor->width * tensor->height * tensor->channels * element_bytes; + return expected_bytes == tensor->bytes ? EMBEDDIP_OK : EMBEDDIP_ERROR_INVALID_SIZE; +} + +embeddip_status_t cv_model_manifest_validate(const cv_model_manifest_t *manifest) +{ + embeddip_status_t status; + + if (manifest == NULL) { + return EMBEDDIP_ERROR_NULL_PTR; + } + if (!string_is_nonempty(manifest->model_id) || !string_is_nonempty(manifest->source_sha256) || + !string_is_nonempty(manifest->generated_sha256) || + !string_is_nonempty(manifest->stedgeai_version) || + !string_is_nonempty(manifest->cube_n6_version) || !string_is_nonempty(manifest->license) || + !string_is_nonempty(manifest->dataset_license) || + !string_is_nonempty(manifest->label_map_id) || + !string_is_nonempty(manifest->training_recipe) || + !string_is_nonempty(manifest->quantization_recipe)) { + return EMBEDDIP_ERROR_INVALID_ARG; + } + if (manifest->deployment_location != CV_DEPLOYMENT_MCU) { + return EMBEDDIP_ERROR_NOT_SUPPORTED; + } + if (manifest->weights_bytes == 0u || manifest->activations_bytes == 0u) { + return EMBEDDIP_ERROR_INVALID_SIZE; + } + if (manifest->weights_region != EMBEDDIP_MEMORY_REGION_EXTERNAL_FLASH) { + return EMBEDDIP_ERROR_NOT_SUPPORTED; + } + if (manifest->activations_region != EMBEDDIP_MEMORY_REGION_FAST_SRAM && + manifest->activations_region != EMBEDDIP_MEMORY_REGION_PSRAM) { + return EMBEDDIP_ERROR_NOT_SUPPORTED; + } + + status = tensor_contract_validate(&manifest->input); + if (status != EMBEDDIP_OK) { + return status; + } + return tensor_contract_validate(&manifest->output); +} diff --git a/runtime/model_manifest.h b/runtime/model_manifest.h new file mode 100644 index 0000000..c6e2ada --- /dev/null +++ b/runtime/model_manifest.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 EmbedDIP + +#ifndef EMBEDDIP_RUNTIME_MODEL_MANIFEST_H +#define EMBEDDIP_RUNTIME_MODEL_MANIFEST_H + +#include + +#include "runtime/runtime.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + CV_DEPLOYMENT_MCU = 0, + CV_DEPLOYMENT_HOST +} cv_deployment_location_t; + +typedef struct { + const char *model_id; + const char *source_sha256; + const char *generated_sha256; + const char *stedgeai_version; + const char *cube_n6_version; + const char *license; + const char *dataset_license; + const char *label_map_id; + const char *training_recipe; + const char *quantization_recipe; + cv_tensor_t input; + cv_tensor_t output; + uint32_t weights_bytes; + uint32_t activations_bytes; + embeddip_memory_region_t weights_region; + embeddip_memory_region_t activations_region; + cv_deployment_location_t deployment_location; +} cv_model_manifest_t; + +embeddip_status_t cv_model_manifest_validate(const cv_model_manifest_t *manifest); + +#ifdef __cplusplus +} +#endif + +#endif /* EMBEDDIP_RUNTIME_MODEL_MANIFEST_H */ diff --git a/runtime/runtime.c b/runtime/runtime.c new file mode 100644 index 0000000..3b6436d --- /dev/null +++ b/runtime/runtime.c @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 EmbedDIP + +#include "runtime/runtime.h" + +#include +#include + +#include "board/common.h" + +static cv_runtime_backend_t runtime_backend; +static int runtime_is_initialized; + +static embeddip_status_t tensor_contract_validate(const cv_tensor_t *tensor) +{ + uint64_t element_bytes; + uint64_t expected_bytes; + + if (tensor->bytes == 0u || tensor->width == 0u || tensor->height == 0u || + tensor->channels == 0u) { + return EMBEDDIP_ERROR_INVALID_SIZE; + } + if (tensor->type != CV_TENSOR_U8 && tensor->type != CV_TENSOR_I8 && + tensor->type != CV_TENSOR_F32) { + return EMBEDDIP_ERROR_NOT_SUPPORTED; + } + if (tensor->layout != CV_TENSOR_HWC && tensor->layout != CV_TENSOR_CHW) { + return EMBEDDIP_ERROR_NOT_SUPPORTED; + } + + element_bytes = tensor->type == CV_TENSOR_F32 ? 4u : 1u; + expected_bytes = (uint64_t)tensor->width * tensor->height * tensor->channels * element_bytes; + if (expected_bytes != tensor->bytes) { + return EMBEDDIP_ERROR_INVALID_SIZE; + } + return EMBEDDIP_OK; +} + +static embeddip_status_t tensor_matches_contract(const cv_tensor_t *tensor, + const cv_tensor_t *contract) +{ + if (tensor->bytes != contract->bytes || tensor->width != contract->width || + tensor->height != contract->height || tensor->channels != contract->channels) { + return EMBEDDIP_ERROR_INVALID_SIZE; + } + if (tensor->type != contract->type || tensor->layout != contract->layout) { + return EMBEDDIP_ERROR_INVALID_FORMAT; + } + return EMBEDDIP_OK; +} + +embeddip_status_t cv_runtime_init(const cv_runtime_backend_t *backend) +{ + embeddip_status_t status; + + if (backend == NULL) { + return EMBEDDIP_ERROR_NULL_PTR; + } + if (backend->invoke == NULL) { + return EMBEDDIP_ERROR_NULL_PTR; + } + status = tensor_contract_validate(&backend->input_contract); + if (status != EMBEDDIP_OK) { + return status; + } + status = tensor_contract_validate(&backend->output_contract); + if (status != EMBEDDIP_OK) { + return status; + } + + runtime_backend = *backend; + runtime_is_initialized = 1; + return EMBEDDIP_OK; +} + +embeddip_status_t cv_runtime_infer(const cv_tensor_t *input, cv_tensor_t *output, + uint32_t *elapsed_cycles) +{ + embeddip_status_t status; + embeddip_status_t invalidate_status = EMBEDDIP_OK; + uint32_t cycles; + + if (!runtime_is_initialized) { + return EMBEDDIP_ERROR_NOT_INITIALIZED; + } + if (input == NULL || output == NULL || input->data == NULL || output->data == NULL) { + return EMBEDDIP_ERROR_NULL_PTR; + } + status = tensor_matches_contract(input, &runtime_backend.input_contract); + if (status != EMBEDDIP_OK) { + return status; + } + status = tensor_matches_contract(output, &runtime_backend.output_contract); + if (status != EMBEDDIP_OK) { + return status; + } + if ((output->flags & EMBEDDIP_BUFFER_READ_ONLY) != 0u) { + return EMBEDDIP_ERROR_INVALID_ARG; + } + + if ((input->flags & EMBEDDIP_BUFFER_NPU_READ) != 0u) { + status = memory_cache_clean(input->data, input->bytes); + if (status != EMBEDDIP_OK) { + return status; + } + } + + tic(); + status = runtime_backend.invoke(runtime_backend.context, input, output); + cycles = toc(); + + if ((output->flags & EMBEDDIP_BUFFER_NPU_WRITE) != 0u) { + invalidate_status = memory_cache_invalidate(output->data, output->bytes); + } + if (elapsed_cycles != NULL) { + *elapsed_cycles = cycles; + } + if (status != EMBEDDIP_OK) { + return status; + } + return invalidate_status; +} diff --git a/runtime/runtime.h b/runtime/runtime.h new file mode 100644 index 0000000..304a0b2 --- /dev/null +++ b/runtime/runtime.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 EmbedDIP + +#ifndef EMBEDDIP_RUNTIME_RUNTIME_H +#define EMBEDDIP_RUNTIME_RUNTIME_H + +#include + +#include "core/error.h" +#include "core/memory_manager.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { CV_TENSOR_U8, CV_TENSOR_I8, CV_TENSOR_F32 } cv_tensor_type_t; +typedef enum { CV_TENSOR_HWC, CV_TENSOR_CHW } cv_tensor_layout_t; + +typedef struct { + void *data; + uint32_t bytes; + uint16_t width; + uint16_t height; + uint16_t channels; + cv_tensor_type_t type; + cv_tensor_layout_t layout; + float scale; + int32_t zero_point; + embeddip_memory_region_t region; + uint32_t flags; +} cv_tensor_t; + +typedef embeddip_status_t (*cv_runtime_invoke_fn)(void *context, const cv_tensor_t *input, + cv_tensor_t *output); + +typedef struct { + void *context; + cv_tensor_t input_contract; + cv_tensor_t output_contract; + cv_runtime_invoke_fn invoke; +} cv_runtime_backend_t; + +embeddip_status_t cv_runtime_init(const cv_runtime_backend_t *backend); +embeddip_status_t cv_runtime_infer(const cv_tensor_t *input, cv_tensor_t *output, + uint32_t *elapsed_cycles); + +#ifdef __cplusplus +} +#endif + +#endif /* EMBEDDIP_RUNTIME_RUNTIME_H */ diff --git a/runtime/stedgeai_n6/backend.c b/runtime/stedgeai_n6/backend.c new file mode 100644 index 0000000..4264b6f --- /dev/null +++ b/runtime/stedgeai_n6/backend.c @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 EmbedDIP + +#include "runtime/stedgeai_n6/backend.h" + +#include + +embeddip_status_t stedgeai_n6_backend_create(const stedgeai_n6_binding_t *binding, + const cv_tensor_t *input_contract, + const cv_tensor_t *output_contract, + cv_runtime_backend_t *out_backend) +{ + embeddip_status_t status; + + if (binding == NULL || input_contract == NULL || output_contract == NULL || + out_backend == NULL || binding->init == NULL || binding->run == NULL) { + return EMBEDDIP_ERROR_NULL_PTR; + } + + status = binding->init(binding->context); + if (status != EMBEDDIP_OK) { + return status; + } + out_backend->context = binding->context; + out_backend->input_contract = *input_contract; + out_backend->output_contract = *output_contract; + out_backend->invoke = binding->run; + return EMBEDDIP_OK; +} diff --git a/runtime/stedgeai_n6/backend.h b/runtime/stedgeai_n6/backend.h new file mode 100644 index 0000000..0427749 --- /dev/null +++ b/runtime/stedgeai_n6/backend.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025 EmbedDIP + +#ifndef EMBEDDIP_RUNTIME_STEDGEAI_N6_BACKEND_H +#define EMBEDDIP_RUNTIME_STEDGEAI_N6_BACKEND_H + +#include "runtime/runtime.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef embeddip_status_t (*stedgeai_n6_init_fn)(void *context); +typedef embeddip_status_t (*stedgeai_n6_run_fn)(void *context, const cv_tensor_t *input, + cv_tensor_t *output); + +typedef struct { + void *context; + stedgeai_n6_init_fn init; + stedgeai_n6_run_fn run; +} stedgeai_n6_binding_t; + +embeddip_status_t stedgeai_n6_backend_create(const stedgeai_n6_binding_t *binding, + const cv_tensor_t *input_contract, + const cv_tensor_t *output_contract, + cv_runtime_backend_t *out_backend); + +#ifdef __cplusplus +} +#endif + +#endif /* EMBEDDIP_RUNTIME_STEDGEAI_N6_BACKEND_H */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8d7888e..beb3a72 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,11 +6,28 @@ add_executable(embeddip_test_image_view test_image_view.c) target_link_libraries(embeddip_test_image_view PRIVATE embedDIP) add_test(NAME embeddip.image_view COMMAND embeddip_test_image_view) +add_executable(embeddip_test_runtime test_runtime.c) +target_link_libraries(embeddip_test_runtime PRIVATE embedDIP) +add_test(NAME embeddip.runtime COMMAND embeddip_test_runtime) + +find_package(Python3 COMPONENTS Interpreter QUIET) +if(Python3_Interpreter_FOUND) + add_test(NAME embeddip.model_manifest + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_model_manifest.py) +endif() + if(EMBEDDIP_TARGET_BOARD STREQUAL "HOST") add_executable(embeddip_test_memory_regions test_memory_regions.c) target_link_libraries(embeddip_test_memory_regions PRIVATE embedDIP) add_test(NAME embeddip.memory_regions COMMAND embeddip_test_memory_regions) + add_executable(embeddip_test_stm32n6_memory test_stm32n6_memory.c) + target_include_directories(embeddip_test_stm32n6_memory PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/fakes/stm32n6 + ${CMAKE_SOURCE_DIR}/board/stm32n6 + ${CMAKE_SOURCE_DIR}) + add_test(NAME embeddip.stm32n6_memory COMMAND embeddip_test_stm32n6_memory) + add_executable(embeddip_test_stm32h7s_memory test_stm32h7s_memory.c) target_include_directories(embeddip_test_stm32h7s_memory PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/fakes/stm32h7s @@ -25,6 +42,23 @@ add_test(NAME embeddip.target_matrix -DEMBEDDIP_BINARY_DIR=${CMAKE_BINARY_DIR}/matrix -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/test_target_matrix.cmake) +add_test(NAME embeddip.stm32n6_profile_missing + COMMAND ${CMAKE_COMMAND} + -DEMBEDDIP_SOURCE_DIR=${CMAKE_SOURCE_DIR} + -DEMBEDDIP_BINARY_DIR=${CMAKE_BINARY_DIR}/n6-missing + -DEMBEDDIP_STM32CUBE_N6_ROOT=/path/that/does/not/exist + -DEMBEDDIP_EXPECT_MISSING_SDK=ON + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/test_stm32n6_profile.cmake) + +if(IS_DIRECTORY "${EMBEDDIP_STM32CUBE_N6_ROOT}") + add_test(NAME embeddip.stm32n6_profile + COMMAND ${CMAKE_COMMAND} + -DEMBEDDIP_SOURCE_DIR=${CMAKE_SOURCE_DIR} + -DEMBEDDIP_BINARY_DIR=${CMAKE_BINARY_DIR}/n6 + "-DEMBEDDIP_STM32CUBE_N6_ROOT=${EMBEDDIP_STM32CUBE_N6_ROOT}" + -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/test_stm32n6_profile.cmake) +endif() + add_test(NAME embeddip.stm32h7s_profile_missing COMMAND ${CMAKE_COMMAND} -DEMBEDDIP_SOURCE_DIR=${CMAKE_SOURCE_DIR} diff --git a/tests/cmake/test_stm32n6_profile.cmake b/tests/cmake/test_stm32n6_profile.cmake new file mode 100644 index 0000000..dfcd153 --- /dev/null +++ b/tests/cmake/test_stm32n6_profile.cmake @@ -0,0 +1,20 @@ +if(NOT DEFINED EMBEDDIP_STM32CUBE_N6_ROOT) + message(FATAL_ERROR "EMBEDDIP_STM32CUBE_N6_ROOT must be provided to this test") +endif() + +execute_process( + COMMAND "${CMAKE_COMMAND}" -S "${EMBEDDIP_SOURCE_DIR}" -B "${EMBEDDIP_BINARY_DIR}" + -DEMBEDDIP_TARGET_BOARD=STM32N6 -DEMBEDDIP_ARCH=ARM -DEMBEDDIP_CPU=CORTEX_M55 + "-DEMBEDDIP_STM32CUBE_N6_ROOT=${EMBEDDIP_STM32CUBE_N6_ROOT}" + RESULT_VARIABLE result OUTPUT_VARIABLE out ERROR_VARIABLE err) + +if(EMBEDDIP_EXPECT_MISSING_SDK) + if(result EQUAL 0) + message(FATAL_ERROR "N6 profile unexpectedly configured with a missing CubeN6 SDK") + endif() + if(NOT "${out}${err}" MATCHES "EMBEDDIP_STM32CUBE_N6_ROOT") + message(FATAL_ERROR "Missing CubeN6 SDK diagnostic did not name EMBEDDIP_STM32CUBE_N6_ROOT: ${out}${err}") + endif() +elseif(NOT result EQUAL 0) + message(FATAL_ERROR "N6 profile did not configure: ${out}${err}") +endif() diff --git a/tests/fakes/stm32n6/stm32n6xx.h b/tests/fakes/stm32n6/stm32n6xx.h new file mode 100644 index 0000000..c5c3b2e --- /dev/null +++ b/tests/fakes/stm32n6/stm32n6xx.h @@ -0,0 +1,9 @@ +#ifndef TESTS_FAKES_STM32N6_STM32N6XX_H +#define TESTS_FAKES_STM32N6_STM32N6XX_H + +#include + +void SCB_CleanDCache_by_Addr(void *address, int32_t size); +void SCB_InvalidateDCache_by_Addr(void *address, int32_t size); + +#endif diff --git a/tests/fixtures/invalid_model_manifest.json b/tests/fixtures/invalid_model_manifest.json new file mode 100644 index 0000000..5da329d --- /dev/null +++ b/tests/fixtures/invalid_model_manifest.json @@ -0,0 +1,9 @@ +{ + "model": {"id": "unit_classifier", "onnx_file": "unit.onnx", "source_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + "generated": {"artifact_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "weights_blob": "generated/unit.xSPI2.bin"}, + "deployment": {"inference_location": "host", "stedgeai_version": "4.0.0", "cube_n6_version": "1.0.0"}, + "legal": {"license": "MIT", "label_map_id": "unit-labels", "dataset_license": "CC0-1.0"}, + "provenance": {"training_recipe": "unit-train-v1", "quantization_recipe": "unit-int8-v1"}, + "io": {"input": {"width": 2, "height": 2, "channels": 3, "type": "u8", "layout": "hwc", "scale": 1.0, "zero_point": 0}, "output": {"width": 1, "height": 1, "channels": 1, "type": "f32", "layout": "hwc", "scale": 1.0, "zero_point": 0}}, + "memory": {"weights_bytes": 64, "activations_bytes": 128, "weights_region": "external_flash", "activations_region": "fast_sram"} +} diff --git a/tests/fixtures/valid_model_manifest.json b/tests/fixtures/valid_model_manifest.json new file mode 100644 index 0000000..f778f4b --- /dev/null +++ b/tests/fixtures/valid_model_manifest.json @@ -0,0 +1,9 @@ +{ + "model": {"id": "unit_classifier", "onnx_file": "unit.onnx", "source_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + "generated": {"artifact_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "weights_blob": "generated/unit.xSPI2.bin"}, + "deployment": {"inference_location": "mcu", "stedgeai_version": "4.0.0", "cube_n6_version": "1.0.0"}, + "legal": {"license": "MIT", "label_map_id": "unit-labels", "dataset_license": "CC0-1.0"}, + "provenance": {"training_recipe": "unit-train-v1", "quantization_recipe": "unit-int8-v1"}, + "io": {"input": {"width": 2, "height": 2, "channels": 3, "type": "u8", "layout": "hwc", "scale": 1.0, "zero_point": 0}, "output": {"width": 1, "height": 1, "channels": 1, "type": "f32", "layout": "hwc", "scale": 1.0, "zero_point": 0}}, + "memory": {"weights_bytes": 64, "activations_bytes": 128, "weights_region": "external_flash", "activations_region": "fast_sram"} +} diff --git a/tests/test_model_manifest.py b/tests/test_model_manifest.py new file mode 100644 index 0000000..9e12e6b --- /dev/null +++ b/tests/test_model_manifest.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 + +import json +import pathlib +import subprocess +import sys +import tempfile + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +TOOL = ROOT / "tools" / "model_manifest.py" +VALID = ROOT / "tests" / "fixtures" / "valid_model_manifest.json" +INVALID = ROOT / "tests" / "fixtures" / "invalid_model_manifest.json" + + +def run(*args): + return subprocess.run([sys.executable, str(TOOL), *map(str, args)], check=False) + + +def compile_rendered(path): + return subprocess.run( + [ + "cc", + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + f"-I{ROOT}", + "-fsyntax-only", + str(path), + ], + check=False, + ) + + +assert run("validate", VALID).returncode == 0 +assert run("validate", INVALID).returncode != 0 + +with tempfile.TemporaryDirectory() as directory: + temporary = pathlib.Path(directory) + baseline = json.loads(VALID.read_text(encoding="utf-8")) + invalid_cases = [] + + extra_record = json.loads(json.dumps(baseline)) + extra_record["unexpected"] = {} + invalid_cases.append(extra_record) + short_hash = json.loads(json.dumps(baseline)) + short_hash["model"]["source_sha256"] = "abc" + invalid_cases.append(short_hash) + bad_type = json.loads(json.dumps(baseline)) + bad_type["io"]["input"]["type"] = "u16" + invalid_cases.append(bad_type) + zero_dimension = json.loads(json.dumps(baseline)) + zero_dimension["io"]["output"]["height"] = 0 + invalid_cases.append(zero_dimension) + zero_bytes = json.loads(json.dumps(baseline)) + zero_bytes["memory"]["weights_bytes"] = 0 + invalid_cases.append(zero_bytes) + missing_field = json.loads(json.dumps(baseline)) + del missing_field["legal"]["license"] + invalid_cases.append(missing_field) + wrong_field_type = json.loads(json.dumps(baseline)) + wrong_field_type["io"]["input"]["scale"] = "1.0" + invalid_cases.append(wrong_field_type) + dimension_overflow = json.loads(json.dumps(baseline)) + dimension_overflow["io"]["input"]["width"] = 65536 + invalid_cases.append(dimension_overflow) + derived_bytes_overflow = json.loads(json.dumps(baseline)) + derived_bytes_overflow["io"]["input"].update( + {"width": 65535, "height": 65535, "channels": 2, "type": "u8"} + ) + invalid_cases.append(derived_bytes_overflow) + weights_overflow = json.loads(json.dumps(baseline)) + weights_overflow["memory"]["weights_bytes"] = 4294967296 + invalid_cases.append(weights_overflow) + activations_overflow = json.loads(json.dumps(baseline)) + activations_overflow["memory"]["activations_bytes"] = 4294967296 + invalid_cases.append(activations_overflow) + positive_zero_point_overflow = json.loads(json.dumps(baseline)) + positive_zero_point_overflow["io"]["input"]["zero_point"] = 2147483648 + invalid_cases.append(positive_zero_point_overflow) + negative_zero_point_overflow = json.loads(json.dumps(baseline)) + negative_zero_point_overflow["io"]["output"]["zero_point"] = -2147483649 + invalid_cases.append(negative_zero_point_overflow) + control_character = json.loads(json.dumps(baseline)) + control_character["legal"]["license"] = "MIT\nBSD" + invalid_cases.append(control_character) + + for index, record in enumerate(invalid_cases): + path = temporary / f"invalid-{index}.json" + path.write_text(json.dumps(record), encoding="utf-8") + assert run("validate", path).returncode != 0 + + render_record = json.loads(json.dumps(baseline)) + render_record["model"]["id"] = "unit-classifier.1" + render_input = temporary / "render.json" + render_output = temporary / "render.c" + render_input.write_text(json.dumps(render_record), encoding="utf-8") + assert run("render-c", render_input, "--output", render_output).returncode == 0 + rendered = render_output.read_text(encoding="utf-8") + assert rendered.count("const cv_model_manifest_t") == 1 + assert "const cv_model_manifest_t embeddip_model_unit_classifier_1 =" in rendered + assert ".deployment_location = CV_DEPLOYMENT_MCU" in rendered + assert "unit.onnx" not in rendered + assert "unit.xSPI2.bin" not in rendered + assert compile_rendered(render_output).returncode == 0 + + for index, model_id in enumerate(("int", "_Foo", "__foo")): + reserved_record = json.loads(json.dumps(baseline)) + reserved_record["model"]["id"] = model_id + reserved_input = temporary / f"reserved-{index}.json" + reserved_output = temporary / f"reserved-{index}.c" + reserved_input.write_text(json.dumps(reserved_record), encoding="utf-8") + assert run("render-c", reserved_input, "--output", reserved_output).returncode == 0 + reserved_rendered = reserved_output.read_text(encoding="utf-8") + sanitized = "".join(character if character.isalnum() else "_" for character in model_id) + assert f"const cv_model_manifest_t embeddip_model_{sanitized} =" in reserved_rendered + assert compile_rendered(reserved_output).returncode == 0 + + boundary_record = json.loads(json.dumps(baseline)) + boundary_record["io"]["input"].update( + {"width": 65535, "height": 1, "channels": 1, "zero_point": -2147483648} + ) + boundary_record["io"]["output"]["zero_point"] = 2147483647 + boundary_record["memory"].update( + {"weights_bytes": 4294967295, "activations_bytes": 4294967295} + ) + boundary_input = temporary / "boundary.json" + boundary_output = temporary / "boundary.c" + boundary_input.write_text(json.dumps(boundary_record), encoding="utf-8") + assert run("render-c", boundary_input, "--output", boundary_output).returncode == 0 + boundary_rendered = boundary_output.read_text(encoding="utf-8") + assert ".width = 65535u" in boundary_rendered + assert ".zero_point = -2147483648" in boundary_rendered + assert ".zero_point = 2147483647" in boundary_rendered + assert ".weights_bytes = 4294967295u" in boundary_rendered + assert ".activations_bytes = 4294967295u" in boundary_rendered + assert compile_rendered(boundary_output).returncode == 0 diff --git a/tests/test_runtime.c b/tests/test_runtime.c new file mode 100644 index 0000000..6802c1c --- /dev/null +++ b/tests/test_runtime.c @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include + +#include "runtime/model_manifest.h" +#include "runtime/runtime.h" +#include "runtime/stedgeai_n6/backend.h" + +static int calls; +static int init_calls; +static int event_count; +static char events[8]; +static embeddip_status_t invoke_result = EMBEDDIP_OK; + +void tic(void) +{ + events[event_count++] = 'T'; +} + +uint32_t toc(void) +{ + events[event_count++] = 't'; + return 1234u; +} + +embeddip_status_t memory_cache_clean(const void *address, size_t size) +{ + assert(address != NULL); + assert(size == 12u); + events[event_count++] = 'C'; + return EMBEDDIP_OK; +} + +embeddip_status_t memory_cache_invalidate(const void *address, size_t size) +{ + assert(address != NULL); + assert(size == 1u); + events[event_count++] = 'I'; + return EMBEDDIP_OK; +} + +static embeddip_status_t mock_invoke(void *context, const cv_tensor_t *input, + cv_tensor_t *output) +{ + assert(context == (void *)0x1234u); + ++calls; + events[event_count++] = 'R'; + ((uint8_t *)output->data)[0] = ((const uint8_t *)input->data)[0]; + return invoke_result; +} + +static cv_tensor_t input_contract(void) +{ + cv_tensor_t tensor = {0}; + tensor.bytes = 12u; + tensor.width = 2u; + tensor.height = 2u; + tensor.channels = 3u; + tensor.type = CV_TENSOR_U8; + tensor.layout = CV_TENSOR_HWC; + return tensor; +} + +static cv_tensor_t output_contract(void) +{ + cv_tensor_t tensor = {0}; + tensor.bytes = 1u; + tensor.width = 1u; + tensor.height = 1u; + tensor.channels = 1u; + tensor.type = CV_TENSOR_U8; + tensor.layout = CV_TENSOR_HWC; + return tensor; +} + +static cv_runtime_backend_t valid_backend(void) +{ + cv_runtime_backend_t backend = {0}; + backend.context = (void *)0x1234u; + backend.input_contract = input_contract(); + backend.output_contract = output_contract(); + backend.invoke = mock_invoke; + return backend; +} + +static void test_runtime_rejects_inference_before_initialisation(void) +{ + uint8_t input_data[12] = {7u}; + uint8_t output_data[1] = {0u}; + cv_tensor_t input = input_contract(); + cv_tensor_t output = output_contract(); + input.data = input_data; + output.data = output_data; + assert(cv_runtime_infer(&input, &output, NULL) == EMBEDDIP_ERROR_NOT_INITIALIZED); +} + +static void test_runtime_rejects_invalid_backends(void) +{ + cv_runtime_backend_t backend = valid_backend(); + assert(cv_runtime_init(NULL) == EMBEDDIP_ERROR_NULL_PTR); + backend.invoke = NULL; + assert(cv_runtime_init(&backend) == EMBEDDIP_ERROR_NULL_PTR); + + backend = valid_backend(); + backend.input_contract.bytes = 0u; + assert(cv_runtime_init(&backend) == EMBEDDIP_ERROR_INVALID_SIZE); + backend = valid_backend(); + backend.output_contract.width = 0u; + assert(cv_runtime_init(&backend) == EMBEDDIP_ERROR_INVALID_SIZE); + backend = valid_backend(); + backend.input_contract.type = (cv_tensor_type_t)99; + assert(cv_runtime_init(&backend) == EMBEDDIP_ERROR_NOT_SUPPORTED); + backend = valid_backend(); + backend.output_contract.layout = (cv_tensor_layout_t)99; + assert(cv_runtime_init(&backend) == EMBEDDIP_ERROR_NOT_SUPPORTED); +} + +static void test_runtime_validates_tensors_and_runs_with_coherency(void) +{ + uint8_t input_data[12] = {7u}; + uint8_t output_data[1] = {0u}; + uint32_t cycles = 0u; + cv_runtime_backend_t backend = valid_backend(); + cv_tensor_t input = backend.input_contract; + cv_tensor_t output = backend.output_contract; + + assert(cv_runtime_init(&backend) == EMBEDDIP_OK); + input.data = input_data; + output.data = output_data; + input.flags = EMBEDDIP_BUFFER_NPU_READ; + output.flags = EMBEDDIP_BUFFER_NPU_WRITE; + calls = 0; + event_count = 0; + assert(cv_runtime_infer(&input, &output, &cycles) == EMBEDDIP_OK); + assert(calls == 1); + assert(output_data[0] == 7u); + assert(cycles == 1234u); + assert(event_count == 5); + assert(memcmp(events, "CTRtI", 5u) == 0); + + input.width = 3u; + assert(cv_runtime_infer(&input, &output, &cycles) == EMBEDDIP_ERROR_INVALID_SIZE); + input = backend.input_contract; + input.data = input_data; + input.type = CV_TENSOR_I8; + assert(cv_runtime_infer(&input, &output, &cycles) == EMBEDDIP_ERROR_INVALID_FORMAT); + input = backend.input_contract; + input.data = input_data; + output.flags = EMBEDDIP_BUFFER_READ_ONLY; + assert(cv_runtime_infer(&input, &output, &cycles) == EMBEDDIP_ERROR_INVALID_ARG); + output = backend.output_contract; + output.data = output_data; + assert(cv_runtime_infer(NULL, &output, &cycles) == EMBEDDIP_ERROR_NULL_PTR); + assert(cv_runtime_infer(&input, NULL, &cycles) == EMBEDDIP_ERROR_NULL_PTR); + input.data = NULL; + assert(cv_runtime_infer(&input, &output, &cycles) == EMBEDDIP_ERROR_NULL_PTR); +} + +static embeddip_status_t binding_init(void *context) +{ + assert(context == (void *)0x5678u); + ++init_calls; + return EMBEDDIP_OK; +} + +static embeddip_status_t binding_run(void *context, const cv_tensor_t *input, + cv_tensor_t *output) +{ + assert(context == (void *)0x5678u); + return mock_invoke((void *)0x1234u, input, output); +} + +static void test_stedgeai_backend_uses_only_explicit_binding(void) +{ + stedgeai_n6_binding_t binding = {(void *)0x5678u, binding_init, binding_run}; + cv_tensor_t input = input_contract(); + cv_tensor_t output = output_contract(); + cv_runtime_backend_t backend = {0}; + uint8_t input_data[12] = {9u}; + uint8_t output_data[1] = {0u}; + + assert(stedgeai_n6_backend_create(NULL, &input, &output, &backend) == + EMBEDDIP_ERROR_NULL_PTR); + binding.run = NULL; + assert(stedgeai_n6_backend_create(&binding, &input, &output, &backend) == + EMBEDDIP_ERROR_NULL_PTR); + binding.run = binding_run; + init_calls = 0; + assert(stedgeai_n6_backend_create(&binding, &input, &output, &backend) == EMBEDDIP_OK); + assert(init_calls == 1); + assert(backend.context == binding.context); + assert(backend.input_contract.bytes == 12u); + assert(backend.output_contract.bytes == 1u); + input.data = input_data; + output.data = output_data; + assert(backend.invoke(backend.context, &input, &output) == EMBEDDIP_OK); + assert(output_data[0] == 9u); +} + +static cv_model_manifest_t valid_manifest(void) +{ + cv_model_manifest_t manifest = {0}; + manifest.model_id = "unit_classifier"; + manifest.source_sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + manifest.generated_sha256 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + manifest.stedgeai_version = "4.0.0"; + manifest.cube_n6_version = "1.0.0"; + manifest.license = "MIT"; + manifest.dataset_license = "CC0-1.0"; + manifest.label_map_id = "unit-labels"; + manifest.training_recipe = "unit-train-v1"; + manifest.quantization_recipe = "unit-int8-v1"; + manifest.input = input_contract(); + manifest.output = output_contract(); + manifest.weights_bytes = 64u; + manifest.activations_bytes = 128u; + manifest.weights_region = EMBEDDIP_MEMORY_REGION_EXTERNAL_FLASH; + manifest.activations_region = EMBEDDIP_MEMORY_REGION_FAST_SRAM; + manifest.deployment_location = CV_DEPLOYMENT_MCU; + return manifest; +} + +static void test_manifest_enforces_local_deployment_contract(void) +{ + cv_model_manifest_t manifest = valid_manifest(); + assert(cv_model_manifest_validate(NULL) == EMBEDDIP_ERROR_NULL_PTR); + assert(cv_model_manifest_validate(&manifest) == EMBEDDIP_OK); + manifest.activations_region = EMBEDDIP_MEMORY_REGION_PSRAM; + assert(cv_model_manifest_validate(&manifest) == EMBEDDIP_OK); + manifest.deployment_location = CV_DEPLOYMENT_HOST; + assert(cv_model_manifest_validate(&manifest) == EMBEDDIP_ERROR_NOT_SUPPORTED); + manifest = valid_manifest(); + manifest.activations_region = EMBEDDIP_MEMORY_REGION_EXTERNAL_FLASH; + assert(cv_model_manifest_validate(&manifest) == EMBEDDIP_ERROR_NOT_SUPPORTED); + manifest = valid_manifest(); + manifest.weights_region = EMBEDDIP_MEMORY_REGION_FAST_SRAM; + assert(cv_model_manifest_validate(&manifest) == EMBEDDIP_ERROR_NOT_SUPPORTED); + manifest = valid_manifest(); + manifest.activations_bytes = 0u; + assert(cv_model_manifest_validate(&manifest) == EMBEDDIP_ERROR_INVALID_SIZE); + manifest = valid_manifest(); + manifest.training_recipe = ""; + assert(cv_model_manifest_validate(&manifest) == EMBEDDIP_ERROR_INVALID_ARG); + manifest = valid_manifest(); + manifest.output.channels = 0u; + assert(cv_model_manifest_validate(&manifest) == EMBEDDIP_ERROR_INVALID_SIZE); +} + +int main(void) +{ + test_runtime_rejects_inference_before_initialisation(); + test_runtime_rejects_invalid_backends(); + test_runtime_validates_tensors_and_runs_with_coherency(); + test_stedgeai_backend_uses_only_explicit_binding(); + test_manifest_enforces_local_deployment_contract(); + return 0; +} diff --git a/tests/test_stm32n6_memory.c b/tests/test_stm32n6_memory.c new file mode 100644 index 0000000..e8297fe --- /dev/null +++ b/tests/test_stm32n6_memory.c @@ -0,0 +1,81 @@ +#include +#include +#include +#include + +#include "core/memory_manager.h" + +alignas(32) uint8_t fast_storage[96]; +alignas(32) uint8_t dma_storage[64]; +alignas(32) uint8_t psram_storage[48]; +alignas(32) uint8_t flash_storage[32]; + +__asm__(".globl __embeddip_fast_sram_start__\n" + ".set __embeddip_fast_sram_start__, fast_storage\n" + ".globl __embeddip_fast_sram_end__\n" + ".set __embeddip_fast_sram_end__, fast_storage + 96\n" + ".globl __embeddip_dma_start__\n" + ".set __embeddip_dma_start__, dma_storage\n" + ".globl __embeddip_dma_end__\n" + ".set __embeddip_dma_end__, dma_storage + 64\n" + ".globl __embeddip_psram_start__\n" + ".set __embeddip_psram_start__, psram_storage\n" + ".globl __embeddip_psram_end__\n" + ".set __embeddip_psram_end__, psram_storage + 48\n" + ".globl __embeddip_xspi_flash_start__\n" + ".set __embeddip_xspi_flash_start__, flash_storage\n" + ".globl __embeddip_xspi_flash_end__\n" + ".set __embeddip_xspi_flash_end__, flash_storage + 32\n"); + +static void *last_clean_address; +static int32_t last_clean_size; +static void *last_invalidate_address; +static int32_t last_invalidate_size; + +void SCB_CleanDCache_by_Addr(void *address, int32_t size) +{ + last_clean_address = address; + last_clean_size = size; +} + +void SCB_InvalidateDCache_by_Addr(void *address, int32_t size) +{ + last_invalidate_address = address; + last_invalidate_size = size; +} + +#include "board/stm32n6/board_stm32n6_memory.c" + +int main(void) +{ + alignas(32) uint8_t cache_span[96]; + void *allocation = (void *)(uintptr_t)1u; + + memory_init(0u); + + assert(n6_allocate_region(EMBEDDIP_MEMORY_REGION_DEFAULT, 40u, 32u, &allocation) == EMBEDDIP_OK); + assert(allocation == fast_storage); + assert(n6_allocate_region(EMBEDDIP_MEMORY_REGION_FAST_SRAM, 64u, 1u, &allocation) == + EMBEDDIP_ERROR_OUT_OF_MEMORY); + assert(allocation == NULL); + + assert(n6_allocate_region(EMBEDDIP_MEMORY_REGION_DMA, 16u, 3u, &allocation) == + EMBEDDIP_ERROR_INVALID_ARG); + assert(allocation == NULL); + assert(n6_allocate_region(EMBEDDIP_MEMORY_REGION_DMA, sizeof(dma_storage), 8u, &allocation) == EMBEDDIP_OK); + assert(allocation == dma_storage); + assert(n6_allocate_region(EMBEDDIP_MEMORY_REGION_PSRAM, sizeof(psram_storage), 16u, &allocation) == EMBEDDIP_OK); + assert(allocation == psram_storage); + assert(n6_allocate_region(EMBEDDIP_MEMORY_REGION_EXTERNAL_FLASH, 1u, 1u, &allocation) == + EMBEDDIP_ERROR_NOT_SUPPORTED); + assert(allocation == NULL); + + assert(embeddip_board_cache_clean(cache_span + 3u, 33u) == EMBEDDIP_OK); + assert(last_clean_address == cache_span); + assert(last_clean_size == 64); + assert(embeddip_board_cache_invalidate(cache_span + 31u, 2u) == EMBEDDIP_OK); + assert(last_invalidate_address == cache_span); + assert(last_invalidate_size == 64); + + return 0; +} diff --git a/tools/model_manifest.py b/tools/model_manifest.py new file mode 100644 index 0000000..ecf3da2 --- /dev/null +++ b/tools/model_manifest.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 + +import argparse +import hashlib +import json +import pathlib + + +TOP_LEVEL_RECORDS = { + "model", + "generated", + "deployment", + "legal", + "provenance", + "io", + "memory", +} +HASH_HEX_LENGTH = hashlib.sha256().digest_size * 2 +UINT16_MAX = (1 << 16) - 1 +UINT32_MAX = (1 << 32) - 1 +INT32_MIN = -(1 << 31) +INT32_MAX = (1 << 31) - 1 + + +def require_record(parent, name, fields): + record = parent.get(name) + if type(record) is not dict: + raise ValueError(f"{name} must be an object") + for field, expected_type in fields.items(): + if field not in record or type(record[field]) is not expected_type: + raise ValueError(f"{name}.{field} must be {expected_type.__name__}") + if expected_type is str and not record[field]: + raise ValueError(f"{name}.{field} must not be empty") + if expected_type is str and any(ord(character) < 0x20 for character in record[field]): + raise ValueError(f"{name}.{field} must not contain JSON control characters") + return record + + +def validate_tensor(io_record, name): + tensor = require_record( + io_record, + name, + { + "width": int, + "height": int, + "channels": int, + "type": str, + "layout": str, + "scale": float, + "zero_point": int, + }, + ) + if tensor["width"] <= 0 or tensor["height"] <= 0 or tensor["channels"] <= 0: + raise ValueError(f"io.{name} dimensions must be positive") + if any(tensor[field] > UINT16_MAX for field in ("width", "height", "channels")): + raise ValueError(f"io.{name} dimensions must fit uint16_t") + if tensor["type"] not in {"u8", "i8", "f32"}: + raise ValueError(f"io.{name}.type is unsupported") + if tensor["layout"] not in {"hwc", "chw"}: + raise ValueError(f"io.{name}.layout is unsupported") + if not INT32_MIN <= tensor["zero_point"] <= INT32_MAX: + raise ValueError(f"io.{name}.zero_point must fit int32_t") + element_bytes = 4 if tensor["type"] == "f32" else 1 + byte_count = tensor["width"] * tensor["height"] * tensor["channels"] * element_bytes + if byte_count > UINT32_MAX: + raise ValueError(f"io.{name} derived byte count must fit uint32_t") + return tensor + + +def validate_manifest(record): + if type(record) is not dict or set(record) != TOP_LEVEL_RECORDS: + raise ValueError("manifest must contain precisely the seven required top-level records") + + model = require_record(record, "model", {"id": str, "onnx_file": str, "source_sha256": str}) + generated = require_record( + record, "generated", {"artifact_sha256": str, "weights_blob": str} + ) + deployment = require_record( + record, + "deployment", + {"inference_location": str, "stedgeai_version": str, "cube_n6_version": str}, + ) + require_record(record, "legal", {"license": str, "label_map_id": str, "dataset_license": str}) + require_record( + record, "provenance", {"training_recipe": str, "quantization_recipe": str} + ) + io_record = require_record(record, "io", {"input": dict, "output": dict}) + input_tensor = validate_tensor(io_record, "input") + output_tensor = validate_tensor(io_record, "output") + memory = require_record( + record, + "memory", + { + "weights_bytes": int, + "activations_bytes": int, + "weights_region": str, + "activations_region": str, + }, + ) + + hexadecimal = set("0123456789abcdefABCDEF") + for name, value in ( + ("model.source_sha256", model["source_sha256"]), + ("generated.artifact_sha256", generated["artifact_sha256"]), + ): + if len(value) != HASH_HEX_LENGTH or any(character not in hexadecimal for character in value): + raise ValueError(f"{name} must contain 64 hexadecimal characters") + if deployment["inference_location"] != "mcu": + raise ValueError("deployment.inference_location must be mcu") + if memory["weights_bytes"] <= 0 or memory["activations_bytes"] <= 0: + raise ValueError("memory byte counts must be positive") + if memory["weights_bytes"] > UINT32_MAX or memory["activations_bytes"] > UINT32_MAX: + raise ValueError("memory byte counts must fit uint32_t") + if memory["weights_region"] != "external_flash": + raise ValueError("memory.weights_region must be external_flash") + if memory["activations_region"] not in {"fast_sram", "psram"}: + raise ValueError("memory.activations_region must be fast_sram or psram") + + return input_tensor, output_tensor + + +def read_and_validate(path): + record = json.loads(path.read_text(encoding="utf-8")) + input_tensor, output_tensor = validate_manifest(record) + return record, input_tensor, output_tensor + + +def c_identifier(model_id): + suffix = "".join( + character + if ("a" <= character <= "z" or "A" <= character <= "Z" or "0" <= character <= "9") + else "_" + for character in model_id + ) + return "embeddip_model_" + suffix + + +def c_string(value): + return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + '"' + + +def tensor_initializer(tensor): + type_names = {"u8": "CV_TENSOR_U8", "i8": "CV_TENSOR_I8", "f32": "CV_TENSOR_F32"} + layout_names = {"hwc": "CV_TENSOR_HWC", "chw": "CV_TENSOR_CHW"} + element_bytes = 4 if tensor["type"] == "f32" else 1 + byte_count = tensor["width"] * tensor["height"] * tensor["channels"] * element_bytes + return ( + "{ .data = NULL, " + f".bytes = {byte_count}u, .width = {tensor['width']}u, .height = {tensor['height']}u, " + f".channels = {tensor['channels']}u, .type = {type_names[tensor['type']]}, " + f".layout = {layout_names[tensor['layout']]}, .scale = {tensor['scale']!r}f, " + f".zero_point = {tensor['zero_point']}, .region = EMBEDDIP_MEMORY_REGION_DEFAULT, .flags = 0u }}" + ) + + +def render_c(record, input_tensor, output_tensor): + model = record["model"] + generated = record["generated"] + deployment = record["deployment"] + legal = record["legal"] + provenance = record["provenance"] + memory = record["memory"] + activation_regions = { + "fast_sram": "EMBEDDIP_MEMORY_REGION_FAST_SRAM", + "psram": "EMBEDDIP_MEMORY_REGION_PSRAM", + } + return f"""// Generated by tools/model_manifest.py; contains metadata only. +#include +#include "runtime/model_manifest.h" + +const cv_model_manifest_t {c_identifier(model['id'])} = {{ + .model_id = {c_string(model['id'])}, + .source_sha256 = {c_string(model['source_sha256'])}, + .generated_sha256 = {c_string(generated['artifact_sha256'])}, + .stedgeai_version = {c_string(deployment['stedgeai_version'])}, + .cube_n6_version = {c_string(deployment['cube_n6_version'])}, + .license = {c_string(legal['license'])}, + .dataset_license = {c_string(legal['dataset_license'])}, + .label_map_id = {c_string(legal['label_map_id'])}, + .training_recipe = {c_string(provenance['training_recipe'])}, + .quantization_recipe = {c_string(provenance['quantization_recipe'])}, + .input = {tensor_initializer(input_tensor)}, + .output = {tensor_initializer(output_tensor)}, + .weights_bytes = {memory['weights_bytes']}u, + .activations_bytes = {memory['activations_bytes']}u, + .weights_region = EMBEDDIP_MEMORY_REGION_EXTERNAL_FLASH, + .activations_region = {activation_regions[memory['activations_region']]}, + .deployment_location = CV_DEPLOYMENT_MCU +}}; +""" + + +def main(): + parser = argparse.ArgumentParser(description="Validate and render EmbedDIP model manifests") + subparsers = parser.add_subparsers(dest="command", required=True) + validate_parser = subparsers.add_parser("validate") + validate_parser.add_argument("manifest", type=pathlib.Path) + render_parser = subparsers.add_parser("render-c") + render_parser.add_argument("manifest", type=pathlib.Path) + render_parser.add_argument("--output", type=pathlib.Path, required=True) + arguments = parser.parse_args() + + try: + record, input_tensor, output_tensor = read_and_validate(arguments.manifest) + if arguments.command == "render-c": + arguments.output.write_text( + render_c(record, input_tensor, output_tensor), encoding="utf-8" + ) + except (OSError, ValueError, json.JSONDecodeError) as error: + parser.error(str(error)) + + +if __name__ == "__main__": + main() diff --git a/tools/test_stm32n6_evidence_snapshots.py b/tools/test_stm32n6_evidence_snapshots.py new file mode 100644 index 0000000..d8ee831 --- /dev/null +++ b/tools/test_stm32n6_evidence_snapshots.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Verify published STM32N6 evidence snapshots match companion sources.""" + +import json +import pathlib + + +ROOT = pathlib.Path(__file__).resolve().parents[1] +COMPANION = ROOT.parent / "examples-stm32n6" +EVIDENCE = ROOT / "docs/benchmarks/evidence" +FOUNDATION = ROOT / "docs/benchmarks/stm32n6-foundation-gate.md" +PAIRS = ( + ( + EVIDENCE / "ch12_local_classifier.json", + COMPANION / "docs/benchmarks/ch12_local_classifier.json", + ), + ( + EVIDENCE / "ch12_local_classifier_manifest.json", + COMPANION / "models/ch12_local_classifier/manifest.json", + ), +) + + +def load(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def main(): + for snapshot, source in PAIRS: + assert snapshot.is_file(), f"missing publication snapshot: {snapshot}" + assert source.is_file(), f"missing companion source: {source}" + assert load(snapshot) == load(source), f"snapshot differs from {source}" + + page = FOUNDATION.read_text(encoding="utf-8") + for snapshot, _source in PAIRS: + relative = snapshot.relative_to(FOUNDATION.parent).as_posix() + assert f"]({relative})" in page, f"foundation page does not link {relative}" + + +if __name__ == "__main__": + main()