前言
STM32N6作为意法半导体推出的首款集成自研神经处理单元的STM32产品以“MCU+NPU”的异构架构重新定义了边缘AI的算力边界,是意法半导体的MCU最前沿技术栈,不过由于其高难度技术应用以及需要的极其深厚的STM32使用经验以及神经网络基础概念,因此上手难度非常的高。

自从STM32N6发布以来,博主有幸获得一块STM32N6570-DK开发板,闲暇之余陆陆续续折腾如何开发。因此将会陆陆续续发表一些使用STM32N6的使用笔记,以供将来的使用者参考。
回顾学习历程,踩了很多很多的坑,在后续使用STM32N6的文章中也会向大家陆续介绍这些点。
使用这块芯片最大的亮点就是在其上运行神经网络单元,本期我们将介绍如何在导入并初步运行和验证我们的神经网络模型。
1
准备工作
在开始之前,先介绍一下我们需要准备哪些东西:首先是一个神经网络模型,这里我选择使用ST提供的目标检测模型(单目标:人)


明确其网络架构,例如选用yolov8n_320_quant_pc_uf_od_coco-person.tflite模型,则代表着模型输入为3203203的RGB888类型图片,输出为单目标模型,F=2100,因此维度信息为:(1,5,2100),模型总大小为2.96MB。

开发工具为STM32CubeIDE用于代码编程,STM32CubeMX用于初始化配置和STM32CubeProgram用于程序烧录。

同时意法半导体还提供了ST Edge AI工具用来帮助神经网络模型量化和辅助部署在STM32N6中,但是操作起来有点麻烦还要去找NPU的驱动库,官方在某一次更新的时候将其功能整合在了STM32CubeMX中,因此可以直接在CubeMX中进行模型处理和部署。
2
STM32CubeMX配置
在上一期的基础上,我们完成了如何在STM32N6中实现FSBL_XIP模式,配置了外部Flash和SPRAM来存放我们的代码。


外部Flash和SPRAM配置如上图所示,具体的配置信息可以参考上一期文章。
神经网络部署
原本在N6中使用NPU和神经网络模型的处理需要依靠ST EdgeAI-Core工具:

操作起来比较繁琐,并且没有没有特别多的社区资料用于参考。但是在某一次CubeMX的更新中,STM32CubeMX中的CubeAI插件也继承了ST Edge AI Core的功能,专用于STM32N6系列模型分析和部署。

首先点击X-CUBE-AI将软件包切换到Application工程,因为我们的模型大小为2.96MB而FSBL空间仅为511K,不足以存放模型,因此要放在更大的Application空间。

Profile用于规定RAM和ROM的使用大小,例如在n6-allmems-O3这个方案中,点击右边的齿轮可以查看当前方案的分配情况:

例如规定外部RAM使用地址从0x90000000开始,外部Flash从0x7100000开始,注意这个信息很重要。外部RAM从0x90000000开始的话,我们在使用的时候就注意,不要造成地址复用,比方说用户在采集摄像头图像的时候,不要存在这个地方,方式和神经网络运算过程中造成内存污染。
外部Flash从0x71000000开始,则代表着神经网络模型权重的下载地址为0x71000000,这个信息后面要用!

根据情况修改内存分配情况,我们暂时不改。

点击分析,STM32CubeMX会调用STEdgeAI的命令来帮助我们部署模型并生成处理报告。
处理报告信息

报告中这部分信息交代了我们的模型使用内存情况,例如该模型使用了npuRAM3,RAM4部分还有2.905MB大小的权重文件位于外部FLASH,首地址为0x71000000。


该部分交代了这个模型的各个部分分别是利用什么来运算,HW为硬件NPU运算,SW为软件CPU运算。
内存管理

在内存配置中开启AXISRAM3和AXISRAM4(我们使用的RAM)
串口调试

开启系统串口1用于调试,注意STM32N6570-DK的USART1分别是PE5和PE6。
3
代码测试
接下来生成我们的工程,在正式进行测试之前,我们要先进行一些准备工作:
串口重定向
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch) PUTCHAR_PROTOTYPE { HAL_UART_Transmit(&huart1, (uint8_t *)ch, 1, 0xFFFF);return ch; }int _write(int fd, char * ptr, int len){ HAL_UART_Transmit(&huart1, (uint8_t *) ptr, len, HAL_MAX_DELAY);return len; }

首先是串口重定向,串口重定向让我们可以用printf来输出USART1的内容,方便我们待会打印内容,注意该步骤需要包含头文件stdio.h。
RIF安全配置
/* USER CODE BEGIN SysInit */ __HAL_RCC_RIFSC_CLK_ENABLE(); RIMC_MasterConfig_t RIMC_master = {0}; RIMC_master.MasterCID = RIF_CID_1; RIMC_master.SecPriv = RIF_ATTRIBUTE_SEC | RIF_ATTRIBUTE_PRIV; HAL_RIF_RIMC_ConfigMasterAttributes(RIF_MASTER_INDEX_NPU, &RIMC_master); HAL_RIF_RISC_SetSlaveSecureAttributes(RIF_RISC_PERIPH_INDEX_NPU, RIF_ATTRIBUTE_PRIV | RIF_ATTRIBUTE_SEC);
/* USER CODE END SysInit */
/* Initialize all configured peripherals */ MX_GPIO_Init(); MX_CACHEAXI_Init(); MX_USART1_UART_Init(); MX_RAMCFG_Init(); MX_X_CUBE_AI_Init(); SystemIsolation_Config(); /* USER CODE BEGIN 2 */
在Application工程中,添加NPU的安全权限,否则系统会直接卡死的。
内存使能
voidMX_X_CUBE_AI_Init(void){ set_clk_sleep_mode(); __HAL_RCC_NPU_CLK_ENABLE(); __HAL_RCC_NPU_FORCE_RESET(); __HAL_RCC_NPU_RELEASE_RESET(); npu_cache_init();/* USER CODE BEGIN 5 */ __HAL_RCC_AXISRAM1_MEM_CLK_ENABLE(); __HAL_RCC_AXISRAM2_MEM_CLK_ENABLE(); __HAL_RCC_AXISRAM3_MEM_CLK_ENABLE(); __HAL_RCC_AXISRAM4_MEM_CLK_ENABLE(); __HAL_RCC_AXISRAM5_MEM_CLK_ENABLE(); __HAL_RCC_AXISRAM6_MEM_CLK_ENABLE();
RAMCFG_SRAM2_AXI->CR &= ~RAMCFG_CR_SRAMSD; RAMCFG_SRAM3_AXI->CR &= ~RAMCFG_CR_SRAMSD; RAMCFG_SRAM4_AXI->CR &= ~RAMCFG_CR_SRAMSD; RAMCFG_SRAM5_AXI->CR &= ~RAMCFG_CR_SRAMSD; RAMCFG_SRAM6_AXI->CR &= ~RAMCFG_CR_SRAMSD;/* USER CODE END 5 */}
在MX_X_CUBE_AI_Init初始化函数中,为内存管理使能。
4
NPU运行代码
voidMX_X_CUBE_AI_Process(void){/* USER CODE BEGIN 6 */ LL_ATON_RT_RetValues_t ll_aton_rt_ret = LL_ATON_RT_DONE;const LL_Buffer_InfoTypeDef * ibuffersInfos = NN_Interface_Default.input_buffers_info();const LL_Buffer_InfoTypeDef * obuffersInfos = NN_Interface_Default.output_buffers_info(); buffer_in = (uint8_t *)LL_Buffer_addr_start(&ibuffersInfos[0]); buffer_out = (uint8_t *)LL_Buffer_addr_start(&obuffersInfos[0]);
// Printing buffer start and end addresses. LL_ATON_RT_RuntimeInit();// run 10 inferencesfor (int inferenceNb = 0; inferenceNb<10; ++inferenceNb) {/* ------------- *//* - Inference - *//* ------------- *//* Pre-process and fill the input buffer */// Fill input buffer with constant data.//_pre_process(buffer_in);/* Perform the inference */ LL_ATON_RT_Init_Network(&NN_Instance_Default); // Initialize passed network instance objectdo {/* Execute first/next step */ ll_aton_rt_ret = LL_ATON_RT_RunEpochBlock(&NN_Instance_Default);/* Wait for next event */if (ll_aton_rt_ret == LL_ATON_RT_WFE) { LL_ATON_OSAL_WFE(); } } while (ll_aton_rt_ret != LL_ATON_RT_DONE);/* Post-process the output buffer *//* Invalidate the associated CPU cache region if requested *///_post_process(buffer_out); LL_ATON_RT_DeInit_Network(&NN_Instance_Default);/* -------------------- *//* - End of Inference - *//* -------------------- */ } LL_ATON_RT_RuntimeDeInit();/* USER CODE END 6 */}
MX_X_CUBE_AI_Process为神经网络推理部分代码,大致可以分为三个部分:输入数据处理、NPU运行、数据后处理。
typedefstruct { constchar *name; /**< Buffer name. NULL if end of list */ __LL_address_t addr_base; /**< Buffer base address */ uint32_t offset_start; /**< Offset of the buffer start address from the base address */ uint32_t offset_end; /**< Offset of the buffer end address from the base address * (first bytes address beyond buffer length) */ uint32_t offset_limit; /**< Offset of the limiter address from the base address, * (needed for configuring streaming engines) */ uint8_t is_user_allocated; /**< */ uint8_t is_param; /**< */ uint16_t epoch; /**< */ uint32_t batch; /**< */ constuint32_t *mem_shape; /**< shape as seen by the user in memory (only valid for input/output buffers) */ uint16_t mem_ndims; /**< Number of dimensions of mem_shape (Length of mem_shape) */ Buffer_CHPos_TypeDef chpos; /**< Position of channels dimension in mem shape */ Buffer_DataType_TypeDef type; /**< */ int8_t Qm; /**< */ int8_t Qn; /**< */ uint8_t Qunsigned; /**< */ uint8_t ndims; /**< */ uint8_t nbits; /**< */ uint8_t per_channel; /**< */ constuint32_t *shape; /**< */ constfloat *scale; /**< */ constint16_t *offset; /**< This can become int8 or uint8 based on the Qunsigned field. * (This field Must have the same format of the quantized value) */ } LL_Buffer_InfoTypeDef;
LL_Buffer_InfoTypeDef结构体包含了模型的基本信息,包括模型运行过程中各类数据缓冲区的核心属性与量化特征。完整记录了缓冲区的命名、基地址、有效地址偏移范围(起始 / 结束 / 限制偏移量)等内存布局信息,也通过is_user_allocated、is_param等标识区分缓冲区的分配归属(用户分配 / 系统分配)和用途类型(参数缓冲区 / 数据缓冲区);同时,epoch、batch字段适配了批量推理、多轮次执行的场景需求。
结构体通过mem_shape、mem_ndims、chpos定义了数据在内存中的实际存储维度、维度数量及通道维度位置,而type字段则标识了缓冲区的数据类型;对于量化模型,Qm、Qn、Qunsigned、nbits、per_channel等字段精准描述了量化参数(量化位数、符号类型、量化系数),scale(缩放因子)、offset(偏移量)字段,完整还原量化张量的数值映射关系,shape字段则补充了模型逻辑层面的张量维度信息。
代码测试
LL_ATON_RT_RetValues_t ll_aton_rt_ret = LL_ATON_RT_DONE; const LL_Buffer_InfoTypeDef * ibuffersInfos = NN_Interface_Default.input_buffers_info(); const LL_Buffer_InfoTypeDef * obuffersInfos = NN_Interface_Default.output_buffers_info(); buffer_in = (uint8_t *)LL_Buffer_addr_start(&ibuffersInfos[0]); buffer_out = (uint8_t *)LL_Buffer_addr_start(&obuffersInfos[0]); // Printing buffer start and end addresses. uint32_t buff_in_len, buff_out_len; printf("输入地址偏移开始 = %lu, \n \r 输入地址偏移结束 = %lu \n \r",ibuffersInfos->offset_start,ibuffersInfos->offset_end); printf("输出地址偏移开始 = %lu, \n \r 输出地址偏移结束 = %lu \n \r",obuffersInfos->offset_start,obuffersInfos->offset_end); // Getting buffer size and printing it. buff_in_len = ibuffersInfos->offset_end - ibuffersInfos->offset_start; buff_out_len = obuffersInfos->offset_end - obuffersInfos->offset_start; printf("输入大小= %lu \n\r 输出大小 size = %lu \n\r", buff_in_len, buff_out_len); LL_ATON_RT_RuntimeInit();
我们打印一下输入缓存和输出缓存的信息内容查看一下结果:


因为我们的模型是3203203的图片,总计307200字节,完全对的上。

输出数据是121005的float类型,所以总大小为121005*4总计42000大小。
NPU运行测试
在进行测试之前,我们还需要把权重文件烧录到单片机的指定地址中(前面设置过的0x71000000)

工程目录下随着STM32CubeMX的生成,有着一个raw格式的文件,我们将其后缀修改为.bin文件:

将其烧录到STM32N6的指定地址中。
for (int inferenceNb = 0; inferenceNb<10; ++inferenceNb) { /* ------------- */ /* - Inference - */ /* ------------- */ /* Pre-process and fill the input buffer */ //_pre_process(buffer_in); /* Perform the inference */ LL_ATON_RT_Init_Network(&NN_Instance_Default); // Initialize passed network instance object do { /* Execute first/next step */ ll_aton_rt_ret = LL_ATON_RT_RunEpochBlock(&NN_Instance_Default); /* Wait for next event */ if (ll_aton_rt_ret == LL_ATON_RT_WFE) { LL_ATON_OSAL_WFE(); } } while (ll_aton_rt_ret != LL_ATON_RT_DONE); /* Post-process the output buffer */ /* Invalidate the associated CPU cache region if requested */ //_post_process(buffer_out);
float *floatout = (float *)buffer_out; for(int i = 0;i<2100;i++) { printf("index:%d [%.2f %.2f %.2f %.2f %.2f]\r\n", i,floatout[i*5],floatout[i*5+1],floatout[i*5+2],floatout[i*5+3],floatout[i*5+4]); }
LL_ATON_RT_DeInit_Network(&NN_Instance_Default); /* -------------------- */ /* - End of Inference - */ /* -------------------- */ } LL_ATON_RT_RuntimeDeInit();
接着我们在代码中添加:把输出地址,按照152100的形状,按照float类型打印出来,模型的运行结果如下:

这5个数据分别对应着Yolov8n模型输出的检测框的:中心横坐标x,中心纵坐标y,检测框宽还有检测框高以及各个类别的置信度,由于我们只有一个类别(人),因此输出就是5个一组。
由于我们的输入数据是随机的,因此输出数据也不可信,但是由于输出的所有结果都在0~1之间,符合Yolo格式的输出结果,因此可以证明神经网络的运行是完全正确的。
后面就是如何运行摄像头,摄像头图像到神经网络输入,输出后处理在屏幕中绘制:
文章出处:实在太懒于是不想取名