DAMOYOLO-S轻量化版本部署在嵌入式设备STM32F103上的探索与实践1. 引言如果你玩过树莓派或者Jetson Nano这类开发板可能会觉得在上面跑个AI模型已经稀松平常了。但今天我们要聊点不一样的把一个人工智能目标检测模型塞进一块只有几十KB内存、主频几十兆赫兹的STM32F103微控制器里。这听起来有点像试图把一头大象装进冰箱。STM32F103也就是我们常说的“蓝桥杯”或者“正点原子”开发板常用的核心芯片资源极其有限。它通常只有20KB的RAM和64KB的Flash性能也远不能和现代处理器相比。而DAMOYOLO-S作为一个轻量化的目标检测模型虽然已经比YOLO系列小了很多但直接放上去依然是天方夜谭。所以这篇教程要解决的就是这个看似不可能的任务。我们将一步步把一个经过“瘦身手术”的DAMOYOLO-S模型部署到STM32F103上让它能实时处理来自摄像头或传感器的图像数据识别出其中的物体。整个过程会涉及到模型压缩、格式转换、内存管理和底层优化我会尽量用大白话讲清楚即使你之前没怎么接触过嵌入式AI也能跟着做下来。2. 准备工作与环境搭建在开始“动手”之前我们得先把“手术台”和“工具”准备好。这个过程有点像拼装一个精密模型缺了哪个零件都不行。2.1 硬件清单首先确认你手头有这些东西一块STM32F103核心板最常见的是STM32F103C8T620KB RAM, 64KB Flash或STM32F103RET664KB RAM, 512KB Flash。后者资源更充裕成功率更高建议新手选用。一个OV7670摄像头模块用于采集图像。这是最经济的选择虽然画质一般但足够我们演示。USB转TTL串口模块用于程序烧录和打印调试信息。杜邦线若干连接以上模块。一台电脑Windows、Linux或macOS都可以。2.2 软件工具链安装我们需要一套专门的工具来为STM32编译和调试代码。安装ARM GCC工具链 这是给ARM芯片编译代码的编译器。去ARM官网下载“GNU Arm Embedded Toolchain”选择适合你操作系统的版本安装。安装后记得把它的bin目录添加到系统的环境变量PATH里这样在命令行就能直接用了。安装STM32CubeMX 这是ST官方出的图形化配置工具能帮我们快速生成芯片的初始化代码比如设置时钟、配置引脚。去ST官网下载安装即可。安装OpenOCD或ST-Link工具 这是用来把编译好的程序烧录到芯片里的工具。如果你用的是ST-Link调试器就用ST官方的ST-Link Utility。如果用的是别的调试器OpenOCD是个开源的好选择。准备模型转换环境Python 我们需要在电脑上对模型进行前期处理。确保你的电脑安装了Python 3.7或以上版本然后通过pip安装以下库pip install tensorflow2.10.0 # 注意版本2.10.x对TFLite Micro支持较好 pip install onnx pip install onnxruntime这里固定TensorFlow版本是因为不同版本对TFLite Micro的支持有差异2.10.x是一个比较稳定的选择。3. 模型瘦身从DAMOYOLO-S到TFLite Micro原始的DAMOYOLO-S模型对于STM32F103来说还是太“胖”了。我们必须对它进行极致的压缩和转换让它变得“骨感”。3.1 模型训练与导出假设你已经有一个用PyTorch训练好的DAMOYOLO-S模型.pt文件。我们的第一步是把它转换成ONNX格式这是一个通用的中间格式。import torch import onnx # 加载你的PyTorch模型 model torch.load(damoyolo-s.pt) model.eval() # 设置为评估模式 # 准备一个示例输入假设输入是320x320的RGB图 dummy_input torch.randn(1, 3, 320, 320) # 导出为ONNX torch.onnx.export(model, dummy_input, damoyolo-s.onnx, export_paramsTrue, opset_version11, # 选择一个稳定的算子集版本 do_constant_foldingTrue, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}) print(模型已导出为 ONNX 格式。)3.2 模型量化与转换量化是模型压缩的关键它将模型参数从32位浮点数float32转换为8位整数int8。这不仅能将模型大小减少约75%还能利用STM32的整数计算单元加速。import tensorflow as tf import onnx from onnx_tf.backend import prepare import numpy as np # 1. 将ONNX模型转换为TensorFlow SavedModel格式需要onnx-tf onnx_model onnx.load(damoyolo-s.onnx) tf_rep prepare(onnx_model) tf_rep.export_graph(damoyolo-s_saved_model) # 2. 加载SavedModel并转换为TFLite格式同时进行量化 converter tf.lite.TFLiteConverter.from_saved_model(damoyolo-s_saved_model) converter.optimizations [tf.lite.Optimize.DEFAULT] # 3. 定义代表性数据集用于校准量化参数准备100张左右训练图片 def representative_dataset_gen(): for _ in range(100): # 这里应该用你的真实预处理后的图片数据 data np.random.rand(1, 320, 320, 3).astype(np.float32) yield [data] converter.representative_dataset representative_dataset_gen # 设置输入输出类型为int8完全整数量化 converter.target_spec.supported_ops [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] converter.inference_input_type tf.int8 converter.inference_output_type tf.int8 # 4. 转换模型 tflite_quant_model converter.convert() # 5. 保存量化后的模型 with open(damoyolo-s_int8.tflite, wb) as f: f.write(tflite_quant_model) print(f量化模型已保存大小{len(tflite_quant_model) / 1024:.2f} KB)经过这一步你应该能得到一个大小在300KB到500KB之间的.tflite文件。但这还不是最终形态我们需要把它转换成C语言数组才能嵌入到单片机程序里。4. 工程创建与内存优化策略现在我们转向嵌入式端在STM32上创建一个项目并思考如何应对紧张的内存资源。4.1 使用STM32CubeMX创建基础工程打开STM32CubeMX选择你的芯片型号如STM32F103C8T6。配置系统时钟SYS将Debug设为Serial Wire否则芯片可能被锁。配置时钟树RCC选择外部高速时钟HSE并将主频HCLK设置到芯片允许的最高值如72MHz榨干性能。配置必要的外设USART1用于串口打印调试信息。模式设为Asynchronous波特率115200。I2C1或DCMI用于连接OV7670摄像头。OV7670通常用SCCB协议类似I2C配置用DCMI接口捕获图像数据。根据你的模块和接线选择。GPIO可能还需要一些普通引脚控制摄像头的复位、电源等。生成代码选择工具链为Makefile然后生成代码。这会得到一个包含HAL库和基础配置的项目文件夹。4.2 将模型集成到工程中转换模型为C数组 使用xxd命令Linux/macOS自带Windows可用Git Bash将.tflite文件转换为C头文件。xxd -i damoyolo-s_int8.tflite model_data.h打开生成的model_data.h你会看到一个巨大的数组比如unsigned char damoyolo_s_int8_tflite[]这就是我们的模型。将模型数据放入Flash 我们需要告诉编译器把这个大数组存放在Flash里const而不是RAM里。修改model_data.h确保数组被声明为const。同时我们可能需要对数组进行对齐以便DMA等操作。// 在model_data.h中 alignas(8) const unsigned char damoyolo_s_int8_tflite[] { // ... 模型数据字节 }; const unsigned int damoyolo_s_int8_tflite_len sizeof(damoyolo_s_int8_tflite);4.3 内存管理技巧STM32F103的RAM是最大的瓶颈。20KB的RAM要同时存放输入图像、中间运算结果激活值、输出结果以及程序运行时的栈和堆。静态内存分配避免使用malloc动态分配因为堆管理本身有开销且容易产生碎片。我们在编译时就确定好每一块内存的用途。内存复用内存池这是最关键的技术。神经网络是一层一层计算的第N层的输出在N1层计算完成后就不再需要了。我们可以把这块内存拿来存放N2层的输入或中间结果。我们需要仔细分析模型的计算图规划出一套内存复用方案。使用特殊内存区域如果芯片有CCM内核耦合内存或DTCM紧耦合内存这部分内存访问速度极快应该用来存放最频繁访问的数据比如当前正在计算的卷积层的输入和输出。降低输入分辨率如果320x320还是太大可以尝试降低到160x160甚至更小这能平方级地减少内存消耗但会牺牲检测精度。5. 利用CMSIS-NN库进行加速CMSIS-NN是ARM专门为Cortex-M系列处理器优化的神经网络内核函数库。它用汇编和SIMD指令精心优化过速度比纯C实现快很多。5.1 集成CMSIS-NN到项目从ARM的GitHub仓库下载CMSIS库或者通过STM32CubeMX的软件包管理器安装。将CMSIS-NN的源码主要是CMSIS/NN和CMSIS/DSP目录下的文件添加到你的工程中。在编译选项中添加CMSIS头文件的路径。5.2 编写模型推理代码TFLite Micro提供了一个解释器但为了极致优化我们可能需要手动调用CMSIS-NN的API来搭建推理流程。这里展示一个简化的思路// 假设我们已经有了量化后的模型参数和规划好的内存池 #include arm_nnfunctions.h // 定义内存池 static int8_t tensor_arena[12 * 1024] __attribute__((aligned(16))); // 12KB内存池16字节对齐 void run_inference(const int8_t* input_image) { // 1. 第一层卷积 (手动调用CMSIS-NN函数示例) // 假设第一层是深度可分离卷积 arm_depthwise_conv_s8(...); arm_fully_connected_s8(...); // 2. 中间层计算... // 每一层计算都使用tensor_arena中预先划分好的区域 // 通过指针偏移来复用内存 // 3. 最后一层输出处理 // 获取边界框和类别得分 process_output((int8_t*)output_tensor, boxes, scores); // 4. 非极大值抑制(NMS) non_max_suppression(boxes, scores, final_detections); }实际上更常见的做法是使用TFLite Micro的框架但为其注册CMSIS-NN的优化内核kernel。这样既能利用TFLite Micro的易用性又能享受CMSIS-NN的速度。你需要实现一个Register_CMSIS_NN函数将卷积、全连接等算子的实现指向CMSIS-NN的函数。6. 实战图像采集与推理演示理论说了这么多是时候看看它到底能不能跑起来了。6.1 摄像头驱动与图像采集OV7670的驱动比较麻烦它需要先通过SCCBI2C配置一大堆寄存器然后通过DCMI接口接收数据。网上有很多现成的驱动代码可以参考。关键步骤是初始化I2C配置OV7670寄存器设置分辨率、像素格式、曝光等。初始化DCMI和DMA让DMA自动将摄像头数据搬运到我们指定的内存缓冲区image_buffer。图像缓冲区通常设置为uint8_t image_buffer[320*240*2]如果是RGB565格式。6.2 图像预处理摄像头采集到的原始数据如RGB565需要转换成模型需要的输入格式通常是int8的RGB并且均值归一化。void preprocess_image(uint8_t* src_rgb565, int8_t* dst_int8_rgb) { // RGB565转RGB888并归一化到int8范围 for (int i 0; i 320 * 240; i) { uint16_t pixel *(uint16_t*)(src_rgb565 i*2); // 提取R、G、B分量 (RGB565) uint8_t r (pixel 11) 0x1F; uint8_t g (pixel 5) 0x3F; uint8_t b pixel 0x1F; // 扩展到0-255范围近似 r (r * 255) / 31; g (g * 255) / 63; b (b * 255) / 31; // 减去均值并量化到int8 (假设均值是128) dst_int8_rgb[i*3] (int8_t)(r - 128); dst_int8_rgb[i*31] (int8_t)(g - 128); dst_int8_rgb[i*32] (int8_t)(b - 128); } }6.3 运行推理与结果解析在主循环中我们不断采集图像预处理然后推理。int main(void) { // 硬件初始化HAL_Init 时钟 GPIO DCMI I2C... // 摄像头初始化 // TFLite Micro解释器初始化并绑定模型和内存池 while (1) { if (image_ready_flag) { // DMA传输完成标志 image_ready_flag 0; // 1. 预处理 preprocess_image(camera_buffer, input_tensor_data); // 2. 推理 TfLiteStatus invoke_status interpreter-Invoke(); if (invoke_status ! kTfLiteOk) { printf(推理失败\n); } // 3. 获取输出并解析 int8_t* output interpreter-output(0)-data.int8; // 解析output得到框的位置、类别和置信度 // 4. 通过串口打印结果例如检测到人坐标(x1,y1,x2,y2)置信度85% printf(Detect: person [%d,%d,%d,%d] score:%d\n, x1, y1, x2, y2, score); } } }7. 总结把DAMOYOLO-S这样的模型部署到STM32F103上确实是一个充满挑战的过程有点像在针尖上跳舞。你需要精打细算地使用每一KB内存小心翼翼地安排每一次计算。整个过程走下来最大的感受就是嵌入式AI和服务器端AI完全是两个世界——这里没有充裕的资源每一个优化都直接关系到成败。从实践来看成功的关键点有几个一是模型的量化必须充分且准确这是减小体积和加速的基础二是内存复用策略要设计得巧妙这是能否跑起来的核心三是合理利用像CMSIS-NN这样的硬件加速库它能带来数倍的性能提升。当然妥协也是必要的比如降低输入分辨率、简化模型的后处理部分。最后的效果虽然帧率可能只有1-2 FPS检测精度也比不上在PC上的版本但看到闪烁的LED灯旁这块小小的芯片能独立地从摄像头画面里认出一个人或者一个杯子那种感觉还是非常奇妙的。它证明了即使在极度受限的环境下AI也能找到自己的一席之地。如果你有兴趣可以从一个更小的模型比如MobileNet SSD开始尝试再逐步挑战更复杂的网络这个过程本身就是一个学习嵌入式系统和AI模型优化的绝佳途径。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。