博主主要在WX写作,C站消息不能及时看见,如有需要联系请关注:《实在太懒于是不想取名》获取联系方式。
STM32N6作为意法半导体推出的首款集成自研神经处理单元的STM32产品以"MCU+NPU"的异构架构重新定义了边缘AI的算力边界,是意法半导体的MCU最前沿技术栈,不过由于其高难度技术应用以及需要的极其深厚的STM32使用经验以及神经网络基础概念,因此上手难度非常的高。

自从STM32N6发布以来,博主有幸获得一块STM32N6570-DK开发板,闲暇之余陆陆续续折腾如何开发。因此将会陆陆续续发表一些使用STM32N6的使用笔记,以供将来的使用者参考。
回顾学习历程,踩了很多很多的坑,在后续使用STM32N6的文章中也会向大家陆续介绍这些点。
上一期我们完成了 IMX335 与 STM32 ISP 中间件的初步对接:通过 STM32_ISP_IQTune 工具调优参数并导出配置,将 ISP 库集成进工程,并实现了传感器信息读取、增益/曝光控制的桥接函数。同时添加了 VSYNC 中断回调,在每帧接收完成后更新帧计数并采集统计数据。最后通过 DCMIPP_ISP_Init 函数启动了完整的图像处理链路,画面从原始的"灰屏"状态有了明显改善,基本呈现出彩色图像。
本期我们将要和前面的内容联动,利用摄像头获取IMX335的图像,经过ISP处理后裁剪成合适的大小,利用NPU实现Yolo神经网络的运算并显示在LTDC显示屏中。
准备工作
具体的内容可以参考本系列的第三篇文章,那里介绍了如何使用STM32CubeMX简单的测试NPU和Yolo模型,这里我们简单回顾一下:
首先是一个神经网络模型,这里我选择使用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中进行模型处理和部署。
CubeMX配置

在上一期我们的摄像头内容中,我们在其基础上进一步配置,导入CubeAI后,添加我们的神经网络模型:
推荐参数修改选择Yes;



我们选择一个320*320的Yolov8n模型导入,点击分析,系统将会调用ST Edge Ai Core工具帮我们处理好模型。

同时,我们在DCMIPP配置中新添加一条流水线PIPE2,我的计划是这样子的:PIPE1用于捕获图像,缩放成800*480的大小用于屏幕显示;PIPE2将原始画面缩放成320*320用于神经网络输入。


同时LTDC开启双图层,底层用于显示摄像头的捕获内容,顶层用于绘制神经网络模型的输出结果。
hdcmipp.Instance = DCMIPP;
if (HAL_DCMIPP_Init(&hdcmipp) != HAL_OK)
{
Error_Handler();
}
/** Pipe 1 Config
*/
pCSI_PipeConfig.DataTypeMode = DCMIPP_DTMODE_DTIDA;
pCSI_PipeConfig.DataTypeIDA = DCMIPP_DT_RAW10;
pCSI_PipeConfig.DataTypeIDB = DCMIPP_DT_RAW10;
if (HAL_DCMIPP_CSI_PIPE_SetConfig(&hdcmipp, DCMIPP_PIPE1, &pCSI_PipeConfig) != HAL_OK)
{
Error_Handler();
}
pCSI_Config.PHYBitrate = DCMIPP_CSI_PHY_BT_1600;
pCSI_Config.DataLaneMapping = DCMIPP_CSI_PHYSICAL_DATA_LANES;
pCSI_Config.NumberOfLanes = DCMIPP_CSI_TWO_DATA_LANES;
HAL_DCMIPP_CSI_SetConfig(&hdcmipp, &pCSI_Config);
pPipeConfig.FrameRate = DCMIPP_FRAME_RATE_ALL;
pPipeConfig.PixelPipePitch = 2400;
pPipeConfig.PixelPackerFormat = DCMIPP_PIXEL_PACKER_FORMAT_RGB888_YUV444_1;
if (HAL_DCMIPP_PIPE_SetConfig(&hdcmipp, DCMIPP_PIPE1, &pPipeConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_DCMIPP_CSI_SetVCConfig(&hdcmipp, 0U, DCMIPP_CSI_DT_BPP10) != HAL_OK)
{
Error_Handler();
}
/** Pipe 2 Config
*/
if (HAL_DCMIPP_CSI_PIPE_SetConfig(&hdcmipp, DCMIPP_PIPE2, &pCSI_PipeConfig) != HAL_OK)
{
Error_Handler();
}
pPipeConfig.PixelPipePitch = 960;
if (HAL_DCMIPP_PIPE_SetConfig(&hdcmipp, DCMIPP_PIPE2, &pPipeConfig) != HAL_OK)
{
Error_Handler();
}
HAL_DCMIPP_CSI_SetVCConfig(&hdcmipp, 1U, DCMIPP_CSI_DT_BPP10);
/* USER CODE BEGIN DCMIPP_Init 2 */
DCMIPP_DownsizeTypeDef DownsizeConf = {0};
DownsizeConf.HRatio = 25656;
DownsizeConf.VRatio = 33161;
DownsizeConf.HSize = 800;
DownsizeConf.VSize = 480;
DownsizeConf.HDivFactor = 316;
DownsizeConf.VDivFactor = 253;
if(HAL_DCMIPP_PIPE_SetDownsizeConfig(&hdcmipp, DCMIPP_PIPE1, &DownsizeConf) != HAL_OK)
{
Error_Handler();
}
if(HAL_DCMIPP_PIPE_EnableDownsize(&hdcmipp, DCMIPP_PIPE1) != HAL_OK)
{
Error_Handler();
}
DownsizeConf.HRatio = 65532; /* 水平缩放比例:12288 */
DownsizeConf.VRatio = 49152; /* 垂直缩放比例:12288 */
DownsizeConf.HSize = 320; /* 输出宽度 */
DownsizeConf.VSize = 320; /* 输出高度 */
DownsizeConf.HDivFactor = 128; /* 水平除法因子:682 */
DownsizeConf.VDivFactor = 180; /* 垂直除法因子:682 */
if(HAL_DCMIPP_PIPE_SetDownsizeConfig(&hdcmipp, DCMIPP_PIPE2, &DownsizeConf) != HAL_OK)
{
Error_Handler();
}
if(HAL_DCMIPP_PIPE_EnableDownsize(&hdcmipp, DCMIPP_PIPE2) != HAL_OK)
{
Error_Handler();
}
首先是DCMIPP配置的相关代码,两条流水线分别把摄像头的数据缩放到800*480和320*320用于屏幕显示和神经网络输入,详细的阅读请参照本系列DCMIPP解释。
STM32N6的开发日记(5):数字摄像头接口像素流水线DCMIPP让MCU拥有高性能摄像头资源
voidDCMIPP_ISP_Init(void)
{
ISP_AppliHelpersTypeDef appliHelpers = {0};
ISP_StatAreaTypeDef statArea = {0};
// 填充 ISP middleware 需要的回调函数
appliHelpers.GetSensorInfo = GetSensorInfoHelper;
appliHelpers.SetSensorGain = SetSensorGainHelper;
appliHelpers.GetSensorGain = GetSensorGainHelper;
appliHelpers.SetSensorExposure = SetSensorExposureHelper;
appliHelpers.GetSensorExposure = GetSensorExposureHelper;
// 统计区域(全画幅)
statArea.X0 = 0;
statArea.Y0 = 0;
statArea.XSize = 2592;
statArea.YSize = 1944;
// 初始化 ISP 中间件
if (ISP_Init(&hcamera_isp, &hdcmipp, 0, &appliHelpers, &statArea, ISP_IQParamCacheInit[0]) != ISP_OK)
{
Error_Handler();
}
// 启动 DCMIPP CSI PIPE
if (HAL_DCMIPP_CSI_PIPE_Start(&hdcmipp, DCMIPP_PIPE1, DCMIPP_VIRTUAL_CHANNEL0,
lcd_bg_buffer, DCMIPP_MODE_CONTINUOUS) != HAL_OK)
{
Error_Handler();
}
// 启动 ISP 处理
if (ISP_Start(&hcamera_isp) != ISP_OK)
{
Error_Handler();
}
while (NbMainFrames < 60)
{
if (ISP_BackgroundProcess(&hcamera_isp) != ISP_OK)
{
}
}
}
启用ISP中间件让画面更加的丰富饱满,详细请参考本系列ISP处理篇章。
STM32N6的开发日记(6):用ISP中间件点亮IMX335相机的专业画质
void MX_LTDC_Init(void)
{
/* USER CODE BEGIN LTDC_Init 0 */
/* USER CODE END LTDC_Init 0 */
LTDC_LayerCfgTypeDef pLayerCfg = {0};
LTDC_LayerCfgTypeDef pLayerCfg1 = {0};
/* USER CODE BEGIN LTDC_Init 1 */
/* USER CODE END LTDC_Init 1 */
hltdc.Instance = LTDC;
hltdc.Init.HSPolarity = LTDC_HSPOLARITY_AL;
hltdc.Init.VSPolarity = LTDC_VSPOLARITY_AL;
hltdc.Init.DEPolarity = LTDC_DEPOLARITY_AL;
hltdc.Init.PCPolarity = LTDC_PCPOLARITY_IPC;
hltdc.Init.HorizontalSync = 4;
hltdc.Init.VerticalSync = 4;
hltdc.Init.AccumulatedHBP = 12;
hltdc.Init.AccumulatedVBP = 12;
hltdc.Init.AccumulatedActiveW = 812;
hltdc.Init.AccumulatedActiveH = 492;
hltdc.Init.TotalWidth = 820;
hltdc.Init.TotalHeigh = 500;
hltdc.Init.Backcolor.Blue = 0;
hltdc.Init.Backcolor.Green = 0;
hltdc.Init.Backcolor.Red = 0;
if (HAL_LTDC_Init(&hltdc) != HAL_OK)
{
Error_Handler();
}
pLayerCfg.WindowX0 = 0;
pLayerCfg.WindowX1 = 800;
pLayerCfg.WindowY0 = 0;
pLayerCfg.WindowY1 = 480;
pLayerCfg.PixelFormat = LTDC_PIXEL_FORMAT_RGB888;
pLayerCfg.Alpha = 255;
pLayerCfg.Alpha0 = 255;
pLayerCfg.BlendingFactor1 = LTDC_BLENDING_FACTOR1_CA;
pLayerCfg.BlendingFactor2 = LTDC_BLENDING_FACTOR2_CA;
pLayerCfg.FBStartAdress = lcd_bg_buffer;
pLayerCfg.ImageWidth = 800;
pLayerCfg.ImageHeight = 480;
pLayerCfg.Backcolor.Blue = 255;
pLayerCfg.Backcolor.Green = 0;
pLayerCfg.Backcolor.Red = 0;
if (HAL_LTDC_ConfigLayer(&hltdc, &pLayerCfg, 0) != HAL_OK)
{
Error_Handler();
}
pLayerCfg1.WindowX0 = 0;
pLayerCfg1.WindowX1 = 800;
pLayerCfg1.WindowY0 = 0;
pLayerCfg1.WindowY1 = 480;
pLayerCfg1.PixelFormat = LTDC_PIXEL_FORMAT_RGB888;
pLayerCfg1.Alpha = 255;
pLayerCfg1.Alpha0 = 255;
pLayerCfg.BlendingFactor1 = LTDC_BLENDING_FACTOR1_CA;
pLayerCfg.BlendingFactor2 = LTDC_BLENDING_FACTOR2_CA;
pLayerCfg1.FBStartAdress = lcd_fg_buffer;
pLayerCfg1.ImageWidth = 800;
pLayerCfg1.ImageHeight = 480;
pLayerCfg1.Backcolor.Blue = 255;
pLayerCfg1.Backcolor.Green = 255;
pLayerCfg1.Backcolor.Red = 0;
if (HAL_LTDC_ConfigLayer(&hltdc, &pLayerCfg1, 1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN LTDC_Init 2 */
if (HAL_LTDC_ConfigLayer_NoReload(&hltdc, &pLayerCfg1, 1) != HAL_OK)
{
Error_Handler();
}
HAL_LTDC_ConfigColorKeying(&hltdc, 0x000, 1);
HAL_LTDC_EnableColoKeying_NoReload(&hltdc, 1);
HAL_LTDC_Reload(&hltdc, LTDC_RELOAD_IMMEDIATE);
/* USER CODE END LTDC_Init 2 */
}
配置完双图层的LTDC设置后,分别指定好Layer0和Layer1的缓存区地址,之后把Layer1设置为颜色键控模式,这里我们讲一下什么叫做颜色键控。

当两张不透明度不为0的图层叠加到一起的时候,上方的图层会覆盖住下方的图层。

调节上方图层的不透明度,将会让下方图层逐步显示出来,但是这种调节是针对整个图层来说的,我们如果想要实现在上方图层进行绘图,下方图层用于显示的效果,就可以使用颜色键控的方法:
颜色键控是 LTDC 针对图层显示的一种颜色过滤功能,它可以让用户指定一种或一个范围的颜色作为透明色,当图层中像素颜色与设定的键控颜色一致时,这部分像素会被硬件直接忽略不参与显示,只显示下层画面或背景,不会占用额外混合计算资源。颜色键控分为单色键控和范围键控两种常用模式,单色键控只匹配完全相同的 RGB 颜色值,范围键控可以覆盖一段 RGB 区间实现相近颜色统一透明处理。
例如我们可以设置上方图层的键控颜色为0x000000(纯黑色),那么当我们的图层是纯黑色时,上方图层就会是透明的。任何不是纯黑的内容,都可以被显示。
NPU部分代码解释
voidMX_X_CUBE_AI_Init(void)
{
__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;
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_RAMCFG_CLK_ENABLE();
RAMCFG_HandleTypeDef hramcfg = {0};
hramcfg.Instance = RAMCFG_SRAM3_AXI;
HAL_RAMCFG_EnableAXISRAM(&hramcfg);
hramcfg.Instance = RAMCFG_SRAM4_AXI;
HAL_RAMCFG_EnableAXISRAM(&hramcfg);
hramcfg.Instance = RAMCFG_SRAM5_AXI;
HAL_RAMCFG_EnableAXISRAM(&hramcfg);
hramcfg.Instance = RAMCFG_SRAM6_AXI;
HAL_RAMCFG_EnableAXISRAM(&hramcfg);
__HAL_RCC_XSPI1_CLK_SLEEP_ENABLE(); /* For display frame buffer */
__HAL_RCC_XSPI2_CLK_SLEEP_ENABLE(); /* For NN weights */
__HAL_RCC_NPU_CLK_SLEEP_ENABLE(); /* For NN inference */
__HAL_RCC_CACHEAXI_CLK_SLEEP_ENABLE(); /* For NN inference */
__HAL_RCC_LTDC_CLK_SLEEP_ENABLE(); /* For display */
__HAL_RCC_DMA2D_CLK_SLEEP_ENABLE(); /* For display */
__HAL_RCC_DCMIPP_CLK_SLEEP_ENABLE(); /* For camera configuration retention */
__HAL_RCC_CSI_CLK_SLEEP_ENABLE(); /* For camera configuration retention */
__HAL_RCC_XSPI1_CLK_SLEEP_ENABLE(); /* For display frame buffer */
__HAL_RCC_XSPI2_CLK_SLEEP_ENABLE(); /* For NN weights */
__HAL_RCC_NPU_CLK_SLEEP_ENABLE(); /* For NN inference */
__HAL_RCC_CACHEAXI_CLK_SLEEP_ENABLE(); /* For NN inference */
__HAL_RCC_LTDC_CLK_SLEEP_ENABLE(); /* For display */
__HAL_RCC_DMA2D_CLK_SLEEP_ENABLE(); /* For display */
__HAL_RCC_DCMIPP_CLK_SLEEP_ENABLE(); /* For camera configuration retention */
__HAL_RCC_CSI_CLK_SLEEP_ENABLE(); /* For camera configuration retention */
LL_ATON_RT_RuntimeInit();
LL_ATON_RT_Init_Network(&NN_Instance_Default);
/* USER CODE END 5 */
}
在初始化阶段中,主要添加了两个部分,一个部分是使能SRAM_AXI,在CubeMX中添加神经网络模型的时候就知道需要使用到哪些部分SRAM。
通过在初始化时使能各外设在SLEEP模式下的时钟,可以确保系统在执行神经网络推理任务时,即便 CPU 进入低功耗睡眠状态,负责数据流转的 AXI 总线、存储权重的 XSPI 以及图像预处理的 DMA2D 等核心硬件依然能保持激活,从而维持整条 AI 动态流水线的连续运行。
#include"dma2d.h"
externuint8_t lcd_fg_buffer[800 * 480 * 3];
extern DMA2D_HandleTypeDef hdma2d;
#include"dcmipp.h"
typedefstruct {
float x1, y1, x2, y2;
float conf;
int keep;
} Box;
__attribute__ ((section (".camera_buf")))
__attribute__ ((aligned (32)))
Box boxes[2100];
voidMX_X_CUBE_AI_Process(void)
{
/* USER CODE BEGIN 6 */
uint32_t buff_in_len,buff_out_len;
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]);
LL_ATON_RT_RuntimeInit();
// Getting buffer size and printing it.
// Getting buffer size and printing it.
buff_in_len = ibuffersInfos->offset_end - ibuffersInfos->offset_start;
buff_out_len = obuffersInfos->offset_end - obuffersInfos->offset_start;
SCB_CleanDCache_by_Addr((uint32_t*)buffer_in, buff_in_len); // 写回内存
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer_in, buff_in_len); // 重新从内存
HAL_DCMIPP_CSI_PIPE_Start(&hdcmipp, DCMIPP_PIPE2, DCMIPP_VIRTUAL_CHANNEL0,
buffer_in, DCMIPP_MODE_SNAPSHOT);
HAL_Delay(5);
SCB_CleanDCache_by_Addr((uint32_t*)buffer_in, buff_in_len); // 写回内存
SCB_InvalidateDCache_by_Addr((uint32_t*)buffer_in, buff_in_len); // 重新从内存取
// run 1 inferences
for (int inferenceNb = 0; inferenceNb<1; ++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;
int valid_count = 0;
for (int i = 0; i < 2100; ++i) {
float cx = floatout[i + 0 * 2100];
float cy = floatout[i + 1 * 2100];
float w = floatout[i + 2 * 2100];
float h = floatout[i + 3 * 2100];
float conf = floatout[i + 4 * 2100];
if (conf > 0.3f) {
float cx_input = cx * 320.0f;
float cy_input = cy * 320.0f;
float w_input = w * 320.0f;
float h_input = h * 320.0f;
boxes[valid_count].x1 = cx_input - w_input / 2.0f;
boxes[valid_count].y1 = cy_input - h_input / 2.0f;
boxes[valid_count].x2 = cx_input + w_input / 2.0f;
boxes[valid_count].y2 = cy_input + h_input / 2.0f;
boxes[valid_count].conf = conf;
boxes[valid_count].keep = 1;
valid_count++;
}
}
for (int i = 0; i < valid_count; i++) {
if (boxes[i].keep) {
for (int j = i + 1; j < valid_count; j++) {
if (boxes[j].keep) {
float x1 = (boxes[i].x1 > boxes[j].x1) ? boxes[i].x1 : boxes[j].x1;
float y1 = (boxes[i].y1 > boxes[j].y1) ? boxes[i].y1 : boxes[j].y1;
float x2 = (boxes[i].x2 < boxes[j].x2) ? boxes[i].x2 : boxes[j].x2;
float y2 = (boxes[i].y2 < boxes[j].y2) ? boxes[i].y2 : boxes[j].y2;
float intersection = (x2 - x1) * (y2 - y1);
if (intersection < 0) intersection = 0;
float area_i = (boxes[i].x2 - boxes[i].x1) * (boxes[i].y2 - boxes[i].y1);
float area_j = (boxes[j].x2 - boxes[j].x1) * (boxes[j].y2 - boxes[j].y1);
float union_area = area_i + area_j - intersection;
float iou = (union_area > 0) ? (intersection / union_area) : 0;
if (iou > 0.7f) {
boxes[j].keep = 0;
}
}
}
}
}
int final_count = 0;
HAL_DMA2D_ConfigLayer(&hdma2d, 1);
HAL_DMA2D_Start(&hdma2d, 0x00000000, (uint32_t)lcd_fg_buffer, 800, 480);
HAL_DMA2D_PollForTransfer(&hdma2d, 1000);
for (int i = 0; i < valid_count; i++) {
if (boxes[i].keep) {
final_count++;
/* 反归一化:320*320 → 800*480 */
/* AI模型输出是320*320的坐标,需要映射到800*480的显示坐标 */
/* 水平缩放比例:800/320 = 2.5 */
/* 垂直缩放比例:480/320 = 1.5 */
int display_x1 = (int)(boxes[i].x1 * 2.5);
int display_y1 = (int)(boxes[i].y1 * 1.5);
int display_x2 = (int)(boxes[i].x2 * 2.5);
int display_y2 = (int)(boxes[i].y2 * 1.5);
int display_width = display_x2 - display_x1;
int display_height = display_y2 - display_y1;
/* 边界检查:确保坐标在屏幕范围内 */
if (display_x1 < 0) display_x1 = 0;
if (display_y1 < 0) display_y1 = 0;
if (display_x2 > 800) display_x2 = 800;
if (display_y2 > 480) display_y2 = 480;
display_width = display_x2 - display_x1;
display_height = display_y2 - display_y1;
if (display_width < 0) display_width = 0;
if (display_height < 0) display_height = 0;
/* 只有当宽度和高度都大于0时才绘制 */
if (display_width > 0 && display_height > 0) {
if (display_y1 >= 480) display_y1 = 479;
if (display_y2 > 480) display_y2 = 480;
if (display_x1 >= 800) display_x1 = 799;
if (display_x2 > 800) display_x2 = 800;
/* 重新计算宽度和高度 */
display_width = display_x2 - display_x1;
display_height = display_y2 - display_y1;
if (display_width <= 0 || display_height <= 0) continue;
/* 绘制上边框 */
if (display_y1 < 480) {
hdma2d.Init.Mode = DMA2D_R2M;
hdma2d.Init.ColorMode = DMA2D_OUTPUT_RGB888;
hdma2d.Init.OutputOffset = 800 - display_width;
hdma2d.Init.RedBlueSwap = DMA2D_RB_REGULAR;
HAL_DMA2D_Init(&hdma2d);
HAL_DMA2D_ConfigLayer(&hdma2d, 1);
HAL_DMA2D_Start(&hdma2d, 0x00FF0000, (uint32_t)&lcd_fg_buffer[(display_y1 * 800 + display_x1) * 3], display_width, 1);
HAL_DMA2D_PollForTransfer(&hdma2d, 100);
}
/* 绘制下边框 */
if (display_y2 <= 480 && display_y2 > display_y1) {
hdma2d.Init.OutputOffset = 800 - display_width;
HAL_DMA2D_Init(&hdma2d);
HAL_DMA2D_Start(&hdma2d, 0x00FF0000, (uint32_t)&lcd_fg_buffer[((display_y2 - 1) * 800 + display_x1) * 3], display_width, 1);
HAL_DMA2D_PollForTransfer(&hdma2d, 100);
}
/* 绘制左边框 */
if (display_x1 < 800) {
hdma2d.Init.OutputOffset = 800 - 1;
HAL_DMA2D_Init(&hdma2d);
HAL_DMA2D_Start(&hdma2d, 0x00FF0000, (uint32_t)&lcd_fg_buffer[(display_y1 * 800 + display_x1) * 3], 1, display_height);
HAL_DMA2D_PollForTransfer(&hdma2d, 100);
}
/* 绘制右边框 */
if (display_x2 <= 800 && display_x2 > display_x1) {
hdma2d.Init.OutputOffset = 800 - 1;
HAL_DMA2D_Init(&hdma2d);
HAL_DMA2D_Start(&hdma2d, 0x00FF0000, (uint32_t)&lcd_fg_buffer[(display_y1 * 800 + (display_x2 - 1)) * 3], 1, display_height);
HAL_DMA2D_PollForTransfer(&hdma2d, 100);
}
}
}
}
printf("总计:%d个目标\r\n", final_count);
LL_ATON_RT_Reset_Network(&NN_Instance_Default);
HAL_Delay(10);
/* -------------------- */
/* - End of Inference - */
/* -------------------- */
}
/* USER CODE END 6 */
}
接着是模型输入、运行、后处理、输出部分,每次开启前,捕获一帧摄像头的输入到神经网络输入地址:Buffer_in,接着运行神经网络,等待运行结束,由于该模型输出是2100*5的大小,总计2500个框,因此我们还需要后处理阶段:
首先通过遍历 2100 个候选锚点提取预测的目标坐标、尺寸及置信度,将归一化的中心点坐标转换为输入尺寸下的矩形框左上角与右下角坐标,并过滤掉置信度低于 0.3 的无效框,随后利用非极大值抑制逻辑,通过计算剩余检测框之间的交并比,将与高置信度框重叠度超过 0.7 的冗余框剔除(标记 keep = 0),从而确保每个检测目标在最终结果中仅保留唯一且最优的检测框。
之后利用DMA2D将模型的输出结果显示在屏幕上:
