☰
HD1910嵌入式AI板实战:YOLOv5部署与性能优化全攻略
2026/9/26 1:18:09 网站建设 项目流程

拿到Microduck-HD1910这块板子的时候,我一开始的念头和大多数人一样:赶紧把手里的YOLOv5模型部署上去,让摄像头识别点东西出来。结果可想而知,模型没跑起来,串口倒是先给我上了生动一课。折腾了整整一个下午才意识到,嵌入式AI开发这活儿,从来都是模型部署、硬件调试、软件开发三件事拧在一起,少一环都玩不转。这篇文章就围绕这三条主线,从板卡能力分析、环境搭建、模型转换、板端推理、硬件排错到服务化封装完整过一遍,把我踩过的坑和验证过的方案都写出来。不管是刚入手HD1910的开发板新手,还是想把自己训练的模型落到实际硬件上的算法工程师,应该都能从里面找到可以直接抄作业的步骤。

1. 先认清Microduck-HD1910的硬件底牌和开发边界

1.1 板卡规格与算力家底

先说清楚这块板子是什么定位。Microduck-HD1910属于典型的嵌入式AI开发板,核心是一颗集成NPU的四核Cortex-A55处理器,主频约1.6GHz,NPU算力在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目录下,用那种也没问题,只是需要把它手动加入PATH:

export PATH=$PATH:/path/to/toolchain/bin export CROSS_COMPILE=aarch64-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需确认是否启用ADB
TFTPtftp -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*6+1], output[i*6+2], output[i*6+3], output[i*6+4]); } return 0; }

看到没,NPU推理程序无非是“初始化、加载模型、填输入、执行、取输出”这五步。真正复杂的部分在预处理和后处理,因为NPU只负责算,图像缩放、归一化、解码、NMS全得自己做。这个认知能帮你避免很多无谓的焦虑。

3. 模型部署核心流程:从PyTorch权重到NPU上的YOLOv5

3.1 为什么不在板子上训练模型

有人会问,既然HD1910能跑Linux,为什么不直接在板子上训练YOLOv5?答案很现实:训练YOLOv5需要GPU和海量内存,就算是最小的yolov5s模型,在只靠CPU的条件下,一个epoch跑几十分钟都很正常。HD1910这种板子的定位是推理设备,不是训练服务器。

正确的工作流是:在PC上用GPU训练模型,导出为通用中间格式,在PC上做量化和转换,最后把转换后的模型文件部署到板子上推理。这样分工明确,也符合企业级生产的流程。

3.2 导出ONNX与算子检查

我用YOLOv5 6.2版本举例,因为这是最成熟、资料最多的版本。训练完成后,用官方脚本导出ONNX:

python 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

转换过程中要特别注意AIPP(AI预处理)配置。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::vector<DetectBox> boxes = decode_yolov5(output, 25200, 80); std::vector<DetectBox> 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::vector<DetectBox> decode_yolov5(float* output, int num_anchors, int num_classes) { std::vector<DetectBox> 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(host="0.0.0.0", port=8080)

这个接口本身很简单,但设计上有几个值得注意的点:

  • 模型只初始化一次,不要在每个请求里加载,否则延迟会非常夸张。
  • 图片缩放、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" b"Content-Type: image/jpeg\r\n\r\n" + buf.tobytes() + b"\r\n") return Response(gen(), mimetype="multipart/x-mixed-replace; boundary=frame")

Web前端只需要一个img标签:

<img src="http://<板子IP>:8080/video" width="640" height="480" />

就能在浏览器里看到实时的检测画面。JPEG质量我压到70,原因是为了节省CPU和带宽;在嵌入式板子上,画质稍微牺牲一点换来流畅度非常值得。MJPEG方案实时延迟在100到200毫秒量级,家庭局域网下足够用。

5.4 开机自启动与模型热切换

服务写好后,还要解决两个工程问题:开机自启和模型热切换。

开机自启用systemd服务非常方便。在板子上创建文件 /etc/systemd/system/duck-infer.service:

[Unit] Description=Microduck HD1910 Inference Service After=network.target [Service] WorkingDirectory=/userdata/server ExecStart=/usr/bin/python3 /userdata/server/app.py Restart=always RestartSec=3 [Install] WantedBy=multi-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上的开发到这里已经比较完整,但值得聊一下后续的扩展方向。目标检测模型不是只能跑YOLOv5,YOLOv8、YOLOv10等演进版本在HD1910上同样可以部署,流程完全一致。如果你手里训练的模型是Qwen或者ChatGLM这种大语言模型,HD1910的算力更适合0.5B到1B级别的小模型,像7B级别的行业微调模型放到这种板子上是不现实的,它更适合放在树莓派5那种算力更高或者干脆走云端推理的路径。

部署新模型到HD1910时,我自己的经验是先在PC端把ONNX的算子和数据排布全部验证清楚,再上板测试,能节省至少一半的排查时间。遇到精度掉点,优先检查量化校准集是否覆盖了真实场景,而不是急着换模型结构。

另外关于视频接入,我最后在项目中做了两种入口:HTTP图片检测接口给业务系统调用,MJPEG视频流给Web展示使用。如果后续有对接NVR或视频平台的需求,可以在HD1910上再跑一个RTSP推流进程,把编码后的H264数据推出去,不过那会让CPU占用率明显升高,需要重新评估分辨率和帧率的取舍。

最后分享一个小技巧:把整个开发流程固化成脚本。我习惯在板子上放一个/deploy目录,里面保存好模型文件、服务代码、依赖清单和启动脚本,这样每次烧录完系统,执行一条命令就能把整个环境恢复出来。嵌入式AI开发看起来环节很多,但把模型部署、硬件调试、软件开发三条线理顺之后,后面再做别的项目,基本就是复制流程、填新参数的事了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询