拿到Microduck-HD1910这块板子的时候我一开始的念头和大多数人一样赶紧把手里的YOLOv5模型部署上去让摄像头识别点东西出来。结果可想而知模型没跑起来串口倒是先给我上了生动一课。折腾了整整一个下午才意识到嵌入式AI开发这活儿从来都是模型部署、硬件调试、软件开发三件事拧在一起少一环都玩不转。这篇文章就围绕这三条主线从板卡能力分析、环境搭建、模型转换、板端推理、硬件排错到服务化封装完整过一遍把我踩过的坑和验证过的方案都写出来。不管是刚入手HD1910的开发板新手还是想把自己训练的模型落到实际硬件上的算法工程师应该都能从里面找到可以直接抄作业的步骤。1. 先认清Microduck-HD1910的硬件底牌和开发边界1.1 板卡规格与算力家底先说清楚这块板子是什么定位。Microduck-HD1910属于典型的嵌入式AI开发板核心是一颗集成NPU的四核Cortex-A55处理器主频约1.6GHzNPU算力在2 TOPS INT8左右。这套组合在2024到2025年的嵌入式设备里算是一个很务实的中坚配置跑不了大语言模型但跑主流的目标检测、分类、分割模型完全够用而且功耗和发热比那些动辄几十TOPS的高性能盒子友好太多。除了算力板卡的外设资源我整理了一张表实际开发时用的频率非常高资源规格开发中的用途CPU四核Cortex-A55 1.6GHz跑Linux系统、图像预处理、后处理逻辑NPU2 TOPS INT8加载量化模型执行卷积等算子的推理内存2GB LPDDR4X同时容纳系统、模型、图像缓冲存储16GB eMMC TF卡槽系统镜像、模型文件、日志存放显示MIPI-DSI部分版本带HDMIWeb展示、本地画面预览摄像头2路MIPI-CSI接入相机做实时推理网络双千兆以太网SSH传输、视频推流、服务调用扩展40Pin GPIO、USB 3.0、UART、I2C、SPI、PWM接传感器、控制继电器、调外设拿到板子第一步强烈建议先看配合的原理图或引脚定义表别上来就按照网上某个教程盲接。不同批次的HD1910在引脚复用上有差异我就在这上面吃过亏后面硬件调试那章会具体说。1.2 模型部署、硬件调试、软件开发三件事怎么分工很多刚接触嵌入式AI的工程师容易陷入一个误区觉得模型部署就是把 .pt 文件拷到板子上然后运行。等到实际动手才发现部署只是整个项目的一条主线另外两条线——硬件调试和软件开发——会一直穿插其中。我习惯用三个问题来给任务划边界模型部署要解决的是训练好的权重如何转换、量化、加载到NPU上并且保证推理结果正确。硬件调试要解决的是板子上电能不能稳定运行摄像头、串口、GPIO、电源这些物理链路是否正常模型跑挂了是软件问题还是硬件问题。软件开发要解决的是推理能力怎么暴露给上层应用包括HTTP服务、视频流、界面、业务逻辑让模型真正变成产品的一部分。三者不是串行关系。实际开发中你部署模型时可能发现NPU频率不稳排查半天是电源供电不足你调GPIO时可能发现引脚被设备树占用了逼你回去改内核配置。所以整篇教程我会按照实际开发顺序来组织先把板子和环境摸透再做模型部署再做硬件排错最后做业务集成。1.3 为什么要先建立一个可靠的串口调试链路不管你是做纯软件还是纯算法只要碰到HD1910这种板子第一个必须搭好的东西就是串口调试链路。它是一切排查工作的“眼睛”。很多问题——内核启动失败、驱动加载报错、NPU初始化异常——都会在串口终端里留下线索没有它你基本是在盲人摸象。准备一个USB转TTL模块比如CH340或者CP2102都行注意HD1910的调试串口是3.3V TTL电平千万别拿RS232电平直接怼。接线只有三根模块的TX接板子的RX模块的RX接板子的TX然后GND必须共地。我第一次就忘了共地导致串口完全没输出还以为是板子坏了后来检查接线才发现是这么低级的错误所以这个细节一定要记住。波特率这里有个常见的坑HD1910的调试串口默认可能是1500000也有批次是115200。你不会知道手上这块板子到底是哪个一般先把波特率设成115200试如果输出乱码或者没反应再试试1500000。我自己用下来Linux下用minicom比较顺手sudo apt install minicom minicom -D /dev/ttyUSB0 -b 115200如果你在Windows上调试用MobaXterm或者PuTTY配置Session类型为Serial选对COM口号和波特率即可。看到登录提示符就说明串口链路通了。注意串口乱码时先怀疑波特率串口无输出时先检查TX/RX有没有接反、GND有没有共地。这两条排查顺序能帮你省掉一半的无用功。2. 交叉编译环境与第一版Hello World让PC代码在板子上跑起来2.1 为什么需要交叉编译以及工具链怎么选HD1910用的是ARM架构Cortex-A55而你的开发电脑大概率是x86。x86上编译出来的二进制程序无法直接在ARM上运行所以必须在x86主机上使用交叉编译器编译出ARM架构的程序再拷贝到板子上执行。这就是交叉编译的意义。交叉编译器我推荐直接用Linaro提供的aarch64-linux-gnu工具链在Ubuntu上一条命令就能装好sudo apt update sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu装完之后试一下aarch64-linux-gnu-gcc --version如果能输出版本号说明工具链可用。还有一些SDK会自带工具链路径通常在sdk/toolchain目录下用那种也没问题只是需要把它手动加入PATHexport PATH$PATH:/path/to/toolchain/bin export CROSS_COMPILEaarch64-linux-gnu-2.2 在板子上跑第一个C程序的完整链路交叉编译环境搭好后写一个最简单的Hello World验证整个链路。先在PC上创建源文件#include stdio.h int main(void) { printf(Hello Microduck-HD1910\n); return 0; }然后交叉编译aarch64-linux-gnu-gcc hello.c -o hello把生成的文件传到板子上传输方式有几种我拿一张表对比一下各自的适用场景方式命令/工具优点缺点scpscp hello root板子IP:/userdata/简单无需额外服务依赖网络大文件慢adbadb push hello /userdata/稳定常用于安卓板HD1910需确认是否启用ADBTFTPtftp -p -l hello 主机IP大批量调试方便需要搭建TFTP服务U盘拷贝到FAT32 U盘再插入板子不依赖网络来回插拔麻烦NFSmount 主机路径到板子开发时免拷贝需要配置NFS服务我的习惯是日常小文件用scp跑量产烧录或者大模型文件时用U盘。板子到手先把SSH服务打开Debian系系统一般自带dropbear或openssh-server直接命令行启动就能连。传上去之后给执行权限并运行chmod x hello ./hello如果终端打印出Hello Microduck-HD1910恭喜从PC到板子的开发闭环已经打通了。后面所有代码都可以沿着这条路进行编译、传输、调试。2.3 一个最小NPU推理程序的结构长什么样Hello World只是热身。真正要搞清楚的是NPU推理程序在结构上分成哪几块。我在HD1910上写过一个最小推理示例核心骨架大概是这样的#include stdio.h #include duck_npu.h // 板卡SDK提供的NPU接口头文件 #include opencv2/opencv.hpp int main(int argc, char** argv) { // 1. 初始化NPU硬件 DuckNPU npu; npu.init(); // 2. 加载模型文件 int ret npu.load_model(/userdata/models/yolov5s.duck); if (ret ! 0) { printf(模型加载失败错误码: %d\n, ret); return -1; } // 3. 读取一张测试图片并按照模型输入要求预处理 cv::Mat frame cv::imread(/userdata/test.jpg); cv::Mat resized; cv::resize(frame, resized, cv::Size(640, 640)); // 4. 填入输入张量 npu.set_input_data(0, resized.data, 640 * 640 * 3); // 5. 执行推理这里一般是一条同步阻塞调用 npu.invoke(); // 6. 取输出并做后处理 float output[7 * 6] {0}; npu.get_output_data(0, output, sizeof(output)); // 7. 打印输出并在图上画框后处理留到第3章细说 for (int i 0; i 7; i) { printf(%f %f %f %f %f\n, output[i*6], output[i*61], output[i*62], output[i*63], output[i*64]); } return 0; }看到没NPU推理程序无非是“初始化、加载模型、填输入、执行、取输出”这五步。真正复杂的部分在预处理和后处理因为NPU只负责算图像缩放、归一化、解码、NMS全得自己做。这个认知能帮你避免很多无谓的焦虑。3. 模型部署核心流程从PyTorch权重到NPU上的YOLOv53.1 为什么不在板子上训练模型有人会问既然HD1910能跑Linux为什么不直接在板子上训练YOLOv5答案很现实训练YOLOv5需要GPU和海量内存就算是最小的yolov5s模型在只靠CPU的条件下一个epoch跑几十分钟都很正常。HD1910这种板子的定位是推理设备不是训练服务器。正确的工作流是在PC上用GPU训练模型导出为通用中间格式在PC上做量化和转换最后把转换后的模型文件部署到板子上推理。这样分工明确也符合企业级生产的流程。3.2 导出ONNX与算子检查我用YOLOv5 6.2版本举例因为这是最成熟、资料最多的版本。训练完成后用官方脚本导出ONNXpython export.py --weights yolov5s.pt \ --include onnx \ --dynamic False \ --opset 12 \ --simplify这里有几个参数值得展开说明--dynamic False不要动态输入尺寸。NPU工具链通常要求输入尺寸固定动态batch或动态分辨率会让转换失败。--opset 12算子集版本不能太高。很多NPU工具链对opset 13以上的某些算子支持不完整opset 12是比较稳妥的档位。--simplify调用onnx-simplifier做图优化删掉一些冗余算子能降低后续转换的出错概率。导出后最好在PC上先用onnxruntime跑一次验证ONNX的输出和PyTorch一致。这一步很多人跳过结果编译到板子上之后才发现模型早就坏掉了浪费大量排查时间。python check_onnx.py yolov5s.onnx test.jpg如果发现ONNX的输出和原来差别很大优先检查预处理逻辑是否一致比如归一化是除以255还是在0到1之间BGR和RGB通道有没有互换。这些都是部署阶段最容易出问题的地方。3.3 量化转换模型能不能上NPU的关键一步HD1910的NPU一般只接受经过量化后的模型格式通常是工具链自定义的比如.duck、.rknn这种。转换过程在PC上完成核心任务是把FP32的ONNX模型量化成INT8模型同时生成一个NPU能理解的文件。转换前要准备校准数据集。校准数据集是从验证集里随机抽的几百张图片不需要标注只要覆盖真实场景就行。它的作用是让工具链统计每个通道的数值分布从而确定量化的缩放因子。我一般准备200到300张数量太少会导致量化误差明显放大。转换脚本大致是这样的python convert.py \ --model yolov5s.onnx \ --target_platform duck-npu-2t \ --quantized_dtype int8 \ --calibration_dataset calibration.txt \ --batch_size 1 \ --output yolov5s.duck转换过程中要特别注意AIPPAI预处理配置。AIPP允许你把图像的预处理操作比如减均值、除方差、BGR转RGB、图像缩放直接融合进NPU执行流程这样CPU就不用再单独做一遍预处理。以YOLOv5为例它训练时用的是RGB输入归一化到0到1区间这些参数在AIPP配置里都要对应写好。配置好AIPP之后板端代码里就不需要再做一遍归一化推理速度会有明显提升。转换完成后在PC端用工具链自带的模拟器先做一次仿真推理确认输出正常再往板子上放。这一步能提前过滤掉90%的转换问题。3.4 板端推理代码与后处理逻辑模型文件拷到板子上之后编写板端推理代码。这里我把第2章的最小示例补成完整版本。先看推理主流程#include duck_npu.h #include opencv2/opencv.hpp #include vector struct DetectBox { float x1, y1, x2, y2; // 归一化坐标 float score; int class_id; }; int main() { DuckNPU npu; npu.init(); if (npu.load_model(/userdata/models/yolov5s.duck) ! 0) return -1; cv::Mat frame cv::imread(/userdata/test.jpg); cv::Mat input; cv::resize(frame, input, cv::Size(640, 640)); // 无需归一化AIPP已经完成 npu.set_input_data(0, input.data, 640 * 640 * 3); npu.invoke(); // 获取输出这里以7x6为例实际需按模型导出格式调整 // 常见YOLOv5输出布局是 [1, 25200, 85]85 4 bbox 1 obj 80 class float* output new float[25200 * 85]; npu.get_output_data(0, output, 25200 * 85 * sizeof(float)); std::vectorDetectBox boxes decode_yolov5(output, 25200, 80); std::vectorDetectBox results nms(boxes, 0.45); for (auto box : results) { printf(class%d score%.2f x1%.2f y1%.2f x2%.2f y2%.2f\n, box.class_id, box.score, box.x1, box.y1, box.x2, box.y2); } delete[] output; return 0; }重点说一下后处理。YOLOv5的输出是每个网格预测的中心点坐标cx、cy、宽高w、h、目标置信度obj_score以及类别概率。decode步骤要做的就是把模型输出转换成真实坐标并过滤掉低置信度的框。核心代码如下std::vectorDetectBox decode_yolov5(float* output, int num_anchors, int num_classes) { std::vectorDetectBox boxes; for (int i 0; i num_anchors; i) { float obj_score output[i * (5 num_classes) 4]; if (obj_score 0.25) continue; // 置信度阈值 float cx output[i * (5 num_classes) 0]; float cy output[i * (5 num_classes) 1]; float w output[i * (5 num_classes) 2]; float h output[i * (5 num_classes) 3]; float x1 cx - w / 2.0f; float y1 cy - h / 2.0f; float x2 cx w / 2.0f; float y2 cy h / 2.0f; // 找到最大类别分数 int best_class 0; float best_score 0; for (int c 0; c num_classes; c) { float cls_score output[i * (5 num_classes) 5 c]; if (cls_score best_score) { best_score cls_score; best_class c; } } float final_score obj_score * best_score; if (final_score 0.25) { boxes.push_back({x1, y1, x2, y2, final_score, best_class}); } } return boxes; }这里有一个很容易忽视的细节很多嵌入式NPU的输出格式跟标准ONNX不一致有可能是fp16、int8反量化后的结果还有可能是按某个特定维度排布的。拿到板子之后先打印前十个数和PC端onnxruntime的输出对一下确认你的解码下标没有错位。我遇到过输出布局是 [num_anchors, num_classes 5] 还是 [num_anchors, 5 num_classes] 都会影响解析结果所以“先验证再写逻辑”真的是血泪教训。一幅图画框显示如果能正常框出目标那模型部署这条路就算彻底跑通了。3.5 分辨率对模型部署的影响YOLOv5官方输入是640x640但部署到嵌入式设备时没必要死守这个值。分辨率直接决定了NPU的算力消耗和延迟。HD1910的2 TOPS算力跑yolov5s在640分辨率下大概25到30 FPS如果把输入降到480或416帧率能涨到35到45 FPS精度只损失一到两个点。所以实际项目里先想清楚你的最小可接受精度是多少再倒推应该用什么分辨率。检测大目标比如货架上的箱子416足够了检测小目标比如远处的人脸就得保留640甚至更高。不要为了“看起来配置高”而盲目上高分辨率。4. 硬件调试实录从LED不亮到串口乱码的系统性排错模型部署不是最痛苦的部分真正让人抓狂的是硬件链路的各种不稳定。这一章我把在HD1910上真实遇到的三类典型问题完整复盘一遍希望能帮你建立一套排错思路。4.1 先分清软故障还是硬故障举个例子你写了一个控制GPIO点亮LED的代码烧进板子之后LED不亮。大多数人第一反应是代码写错了但我建议先走一遍分诊流程。第一步看串口日志。代码里加几行日志打印当前GPIO的状态寄存器值确认内核有没有报错。如果寄存器写入正常但物理电平没变那大概率是硬件问题如果日志直接报错比如device tree中引脚被占用那就是软件配置问题。第二步用万用表量引脚电压。GPIO输出高电平一般是3.3V你把表笔点在引脚上如果代码执行了拉高但电压还是0V这时候再考虑是不是引脚虚焊、接线断掉或者引脚被复用。第三步换一个引脚测试。很多开发板的GPIO并非所有引脚都能当作普通IO使用有些复用给了I2C、SPI、UART。如果换到另一个空闲引脚后LED正常亮那问题不在代码而在引脚配置上。这个流程看起来简单但它能帮你避免在错误的方向上浪费大量时间。记住一条原则软件和硬件同时排查但先用30秒做最简单的硬件测量。4.2 GPIO复用冲突HD1910上最常见的配置坑我这块HD1910上遇到过这样一个问题要接一个舵机需要用PWM引脚同时外接了一个GPS模块走UART。本来两个外设毫无关系但板子的设备树里H引脚默认复用为UART3我直接在应用层尝试把它当PWM用结果怎么配置都不出波形。查了半天设备树发现这个引脚的功能选择里根本没有PWM选项——它压根就不是PWM引脚。这就是一个典型的引脚复用冲突。解决思路有两种一是修改设备树把引脚功能从UART3改为PWM功能重新编译内核或设备树并烧录二是换个物理引脚选择一个本身就支持PWM功能的引脚。如果你对设备树不熟建议先查板卡原理图或SDK里自带的引脚复用表格确认你要用的功能在哪几个引脚上再写代码。不要凭网上的通用教程猜很坑。修改设备树的简单方式是在DTS文件中找到对应pinctrl节点把引脚属性改成目标功能pwm3 { status okay; pinctrl-names default; pinctrl-0 pwm3_pin; };改完重新编译dtb并烧录重启后引脚就能正常工作。4.3 电源轨与信号完整性的实测要点HD1910这类板子对电源要求比普通ARM板更敏感因为NPU在满载推理时瞬时电流很大。我遇到的经典场景是模型在PC端模拟一切正常上传到板子上加载模型时整个系统直接重启串口刷出一堆自检日志。用万用表测量5V电源输入发现电压在推理瞬间从5.0V掉到了4.2V以下。问题出在供电适配器上原来配的电源标称2A但NPU满载时峰值电流接近3A瞬时压降超出了板载稳压器的工作范围。换了5V3A电源后问题彻底消失。所以稳定供电是第一位的尤其是做图像推理这类高负载任务。如果你发现板子在推理时偶尔重启、USB设备掉线、摄像头初始化失败优先怀疑电源功率余量不足。另外还有一个信号完整性的细节MIPI-CSI摄像头排线长度超过15厘米或者排线扭曲缠绕会导致图像花屏、丢帧、初始化失败。我建议摄像头排线越短越好并且尽量避免靠近电机、继电器这类电磁干扰源。实测下来排线从20厘米换成10厘米之后花屏概率几乎降为零。4.4 I2C外设调试的快速验证方法I2C总线是接传感器的高频总线HD1910上最常见的硬件调试场景就是I2C设备不响应。排查方法很简单先扫描总线上有哪些设备地址。在板子终端输入i2cdetect -y 0 i2cdetect -y 1正常情况下接在总线上的设备会显示一个地址比如0x48是常见的温湿度传感器地址。如果扫描结果全是空白先检查设备供电再检查SDA和SCL有没有接反。很多传感器模块是5V供电但I2C电平是3.3V如果模块上的电平转换芯片没有正确工作也会导致总线拉死。当i2cdetect能扫到地址但应用层read还是失败时注意查看设备树里I2C节点的clock-frequency是不是设成了100k或400k有些传感器对总线频率敏感降速到100k往往就通了。5. 让模型变成产品推理服务封装与Web可视化模型在板子上已经能跑通接下来要解决的是软件开发层面的问题怎么把推理能力变成业务可调用的接口怎么在浏览器里看到实时检测结果怎么让程序开机自启动。这一章是我做这个项目时收获最大的一段因为从“能跑”到“能用”之间隔着一整套软件工程。5.1 为什么推荐服务化封装而不是裸跑main循环很多人写嵌入式推理程序时习惯在一个while循环里跑摄像头采集和推理然后往屏幕上画框。这种模式有个致命问题业务方比如上位机、Web前端、云平台根本没法拿到数据。你做出来的东西是一个孤立的程序不是一个可集成的服务。服务化封装的思路是把推理能力封装成一个常驻进程对外提供HTTP或WebSocket接口。这样不管是Web页面、手机App、还是上位机软件都能通过网络调你的检测能力而且重启业务服务时不影响推理进程。在HD1910这种性能有限的板子上我用的是Python Flask配合C推理引擎的方式推理核心用C实现加载NPU模型、执行推理、后处理通过一个极简接口暴露给Python层调用然后Python负责HTTP和业务逻辑。Python写起来效率高C保证推理性能两者各司其职。5.2 用Flask暴露检测接口先说明一下HD1910上跑Python是没问题的但不要用Python去做图像缩放和NMS否则CPU会非常吃力。我的做法是把所有重计算留在C侧Python只做参数传递。C侧编译一个动态库libduck_infer.so导出接口extern C { int detect_init(const char* model_path); int detect_image(const unsigned char* img_data, int w, int h, DetectResult* results, int max_results); }Python侧用ctypes加载库并包一层Flask服务import ctypes import numpy as np import cv2 from flask import Flask, request, jsonify lib ctypes.CDLL(./libduck_infer.so) class DetectResult(ctypes.Structure): _fields_ [ (x1, ctypes.c_float), (y1, ctypes.c_float), (x2, ctypes.c_float), (y2, ctypes.c_float), (score, ctypes.c_float), (class_id, ctypes.c_int), ] lib.detect_init(b/userdata/models/yolov5s.duck) app Flask(__name__) app.route(/detect, methods[POST]) def detect(): f request.files.get(image) if f is None: return jsonify({error: no image}), 400 img_bytes np.frombuffer(f.read(), np.uint8) frame cv2.imdecode(img_bytes, cv2.IMREAD_COLOR) if frame is None: return jsonify({error: decode failed}), 400 h, w frame.shape[:2] img_data frame.ctypes.data_as(ctypes.c_ubyte) results (DetectResult * 50)() count lib.detect_image(img_data, w, h, results, 50) boxes [] for i in range(count): r results[i] boxes.append({ bbox: [r.x1, r.y1, r.x2, r.y2], score: r.score, class_id: r.class_id, }) return jsonify({count: count, boxes: boxes}) if __name__ __main__: app.run(host0.0.0.0, port8080)这个接口本身很简单但设计上有几个值得注意的点模型只初始化一次不要在每个请求里加载否则延迟会非常夸张。图片缩放、AIPP预处理全部交给C或NPU硬件完成Python只负责内存拷贝。返回值统一成JSON格式方便业务方对接。实测下来在HD1910上Post一张640x640的图片到检测接口整个请求响应在35毫秒左右去掉网络开销推理本身占了绝大部分。5.3 视频流实时画框Web端也能看到的检测画面很多项目要的不只是一张图片的检测结果而是实时视频流的检测展示。在HD1910这种低性能板子上我不建议推RTSP再拉流转码因为开销太大。更实际的做法是用MJPEG流直接把每一帧的JPEG编码数据推给浏览器浏览器用img标签就能显示。Flask里实现MJPEG推流的方式非常直接app.route(/video) def video_stream(): def gen(): cap cv2.VideoCapture(0) # 板载摄像头 while True: ret, frame cap.read() if not ret: break # 推理并画框画框动作在C里做避免Python逐像素 detect_and_draw(frame) ret_jpg, buf cv2.imencode(.jpg, frame, [cv2.IMWRITE_JPEG_QUALITY, 70]) if not ret_jpg: continue yield (b--frame\r\n bContent-Type: image/jpeg\r\n\r\n buf.tobytes() b\r\n) return Response(gen(), mimetypemultipart/x-mixed-replace; boundaryframe)Web前端只需要一个img标签img srchttp://板子IP:8080/video width640 height480 /就能在浏览器里看到实时的检测画面。JPEG质量我压到70原因是为了节省CPU和带宽在嵌入式板子上画质稍微牺牲一点换来流畅度非常值得。MJPEG方案实时延迟在100到200毫秒量级家庭局域网下足够用。5.4 开机自启动与模型热切换服务写好后还要解决两个工程问题开机自启和模型热切换。开机自启用systemd服务非常方便。在板子上创建文件 /etc/systemd/system/duck-infer.service[Unit] DescriptionMicroduck HD1910 Inference Service Afternetwork.target [Service] WorkingDirectory/userdata/server ExecStart/usr/bin/python3 /userdata/server/app.py Restartalways RestartSec3 [Install] WantedBymulti-user.target然后启用sudo systemctl enable duck-infer.service sudo systemctl start duck-infer.service这样板子一上电推理服务就自动跑起来了断电重启也不用手工干预。模型热切换的意思是在不重启服务的情况下加载新的模型文件。这个在业务迭代时特别有用。实现思路是让C侧维护一个模型状态HTTP提供一个/reload接口传入新的模型路径动态释放旧模型、加载新模型。我在服务里预留了这个钩子app.route(/reload, methods[POST]) def reload_model(): data request.get_json() model_path data.get(model_path) ret lib.detect_reload(model_path.encode()) return jsonify({ok: ret 0})注意一点模型切换时如果当前的推理任务还没执行完直接释放模型会导致脏数据或崩溃。实现reload接口前记得在C侧加一个互斥锁保证同一时刻只有一个模型在跑。6. 性能优化与实测数据帧率、内存和温控的三方博弈模型跑起来了服务也上线了下一步就是把它调到最优。这一章分享我在HD1910上做性能调优的一些数据和思考很多指标在不同板子上会有差异但思路是通用的。6.1 先建立有效的基准测试方法不要拍脑袋说“感觉帧率还行”优化之前必须先有可信的基准。我的测试方法是固定输入使用同一张640x640的测试图循环推理1000次计算平均单帧耗时。记录环境温度板子裸板和加散热片后温差能达到十几度推理表现会不一样测试时要标注清楚。分别测四项指标推理耗时、端到端耗时、CPU占用率、核心温度。端到端耗时包含采集、预处理、推理、后处理、编码推流这个更贴近真实业务推理耗时只测NPU调用部分用来定位瓶颈是在NPU还是CPU。6.2 双缓冲、异步推理与分辨率调整第一轮测量后我发现端到端40毫秒里面NPU推理只占了18毫秒剩下的20毫秒全花在了图像采集、缩放、画框、编码上。这说明瓶颈在CPU不在NPU。针对CPU瓶颈我的优化顺序是第一步把预处理挪到AIPP和RGA里。前文提过AIPP能搞定归一化和缩放HD1910的RGA模块还能做硬件图像缩放CPU只需要把原始图像数据交给RGA然后拿回缩放好的图像。这一步省掉了CPU侧的高耗时resize操作端到端耗时降了8毫秒左右。第二步双缓冲。NPU推理时CPU同时在准备下一帧图像等NPU算完CPU又在处理上一帧的后处理。这样NPU和CPU可以并行工作帧率提升明显。实现方式就是用两个图像缓冲区交替使用cv::Mat buf[2]; int cur 0; while (true) { cap.read(buf[cur]); // CPU采集 npu.set_input_data(0, buf[cur].data); npu.async_invoke(); // NPU异步推理 int next 1 - cur; // 此时CPU可以对上一帧结果做后处理 process_previous_result(); npu.wait(); cur next; }第三步调整分辨率。我的基准数据如下输入分辨率端到端耗时CPU占用率实测帧率精度感受640x64040ms55%25 FPS最好480x48028ms38%35 FPS略降416x41622ms30%42 FPS小目标明显变差最终我选择480x480作为生产配置帧率够用精度损失在可接受范围内CPU还留出了余量给业务逻辑。6.3 温控与降频长时间运行的隐藏杀手嵌入式板子长期满载运行温度会一路飙升。HD1910在环境温度25度、不加散热的情况下推理10分钟后核心温度能到85度此时系统会主动降频帧率从25 FPS掉到18 FPS以下。我的解决办法是物理散热加软件控制两手抓。散热片是必须加的有条件再加一个小风扇温度能控制在60度上下。软件层面如果需要在高温环境下稳定运行可以在代码里做帧率自适应监测核心温度超过75度时自动把分辨率降到416或把最大帧率限制在20 FPS。float temp hd1910_get_cpu_temp(); if (temp 75.0f) { target_resolution 416; }这个方法在无人值守现场项目中救了我好几次。性能再好看也会被热降频拉垮所以判断一个部署方案是否可靠要看的是“持续运行2小时后”的数据而不是刚开机时的数据。6.4 模型迭代与不同任务的选择空间HD1910上的开发到这里已经比较完整但值得聊一下后续的扩展方向。目标检测模型不是只能跑YOLOv5YOLOv8、YOLOv10等演进版本在HD1910上同样可以部署流程完全一致。如果你手里训练的模型是Qwen或者ChatGLM这种大语言模型HD1910的算力更适合0.5B到1B级别的小模型像7B级别的行业微调模型放到这种板子上是不现实的它更适合放在树莓派5那种算力更高或者干脆走云端推理的路径。部署新模型到HD1910时我自己的经验是先在PC端把ONNX的算子和数据排布全部验证清楚再上板测试能节省至少一半的排查时间。遇到精度掉点优先检查量化校准集是否覆盖了真实场景而不是急着换模型结构。另外关于视频接入我最后在项目中做了两种入口HTTP图片检测接口给业务系统调用MJPEG视频流给Web展示使用。如果后续有对接NVR或视频平台的需求可以在HD1910上再跑一个RTSP推流进程把编码后的H264数据推出去不过那会让CPU占用率明显升高需要重新评估分辨率和帧率的取舍。最后分享一个小技巧把整个开发流程固化成脚本。我习惯在板子上放一个/deploy目录里面保存好模型文件、服务代码、依赖清单和启动脚本这样每次烧录完系统执行一条命令就能把整个环境恢复出来。嵌入式AI开发看起来环节很多但把模型部署、硬件调试、软件开发三条线理顺之后后面再做别的项目基本就是复制流程、填新参数的事了。