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

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


明确其网络架构,例如选用yolov8n_320_quant_pc_uf_od_coco-person.tflite模型,则代表着模型输入为320*320*3的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 inferences
for (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 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);
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();
我们打印一下输入缓存和输出缓存的信息内容查看一下结果:


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

输出数据是1*2100*5的float类型,所以总大小为1*2100*5*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();
接着我们在代码中添加:把输出地址,按照1*5*2100的形状,按照float类型打印出来,模型的运行结果如下:

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