云计算百科
云计算领域专业知识百科平台

成本砍到1/10!威哥STM32H7+OV5640+C# YOLOv8边缘推理实战:裸机协同上位机毫秒级检测

上个月天津滨海的李老板找我,愁眉苦脸的:“威哥,之前那套T400工控机的检测方案好用是好用,但我们要做100台便携式设备去现场巡检,一台工控机2000块,成本实在扛不住,能不能搞个便宜点的嵌入式方案?”

我当时拍胸脯说没问题,但心里其实没底——STM32H7虽然是Cortex-M7里的扛把子,主频400MHz,但要跑完整的YOLOv8还是太勉强了。后来蹲在实验室里熬了三个通宵,想了个“分工协作”的法子:STM32H7裸机负责采集图像、预处理和简单控制,C#上位机(用个便宜的树莓派4或者迷你PC就行)负责跑YOLOv8推理和后处理,两者用以太网通信,成本直接砍到200块一套,速度还不慢。

今天就跟大家唠唠这中间的具体实现和踩的坑,从硬件选型到STM32裸机代码,再到C#上位机的协同,全是能直接落地的干货。


先定架构:别硬让STM32跑推理,分工才是王道

一开始我也想过用NPU或者把YOLOv8量化到极致跑在STM32上,但试了一下——YOLOv8n量化到INT8后,用STM32H7的CMSIS-NN库跑,一帧320×320的图像要2秒多,根本没法用。

后来换了思路,把任务拆成两半:

  • 边缘端(STM32H7+OV5640):
  • 用DCMI接口+DMA采集OV5640的RGB565图像,直接存到SRAM,不占CPU;
  • 做轻量级预处理:把RGB565转成RGB888,Resize到320×320(用双线性插值太费CPU,我用了最近邻插值,速度快,精度损失也不大);
  • 用W5500以太网模块把预处理后的图像数据发给上位机;
  • 接收上位机的检测结果,控制LED灯(合格亮绿灯,不合格亮红灯)。
  • 上位机端(C# + TensorRT):
  • 用TCP协议接收STM32发过来的图像数据,重组帧;
  • 把图像数据转成Tensor,用之前优化好的YOLOv8n INT8模型推理;
  • 解析检测框,画框显示;
  • 把检测结果发回给STM32。

这样分工后,STM32只做自己擅长的“采集+传输+控制”,推理交给上位机,成本低,速度还快——实测总延迟60ms左右,完全满足现场巡检的需求。


硬件部分:选对芯片和模块,少走一半弯路

硬件选型我踩了不少坑,一开始用了USB摄像头,结果STM32的USB Host驱动写起来太麻烦,还不稳定;后来换了DCMI接口的OV5640,终于顺了。给大家列一下最终的硬件清单:

硬件模块型号/规格价格说明
主控芯片 STM32H743VIT6 80元 主频400MHz,有DCMI接口,SRAM够大(1MB)
摄像头 OV5640(DCMI接口) 30元 500万像素,支持RGB565输出,初始化简单
以太网模块 W5500(SPI接口) 25元 硬件TCP/IP协议栈,不用STM32跑协议,省CPU
电源模块 5V/2A DC-DC 10元 给STM32、摄像头、以太网模块供电
LED灯+电阻 红/绿LED各一个,220Ω电阻 2元 显示检测结果
其他(PCB、杜邦线) 53元

踩坑1:DCMI的时序配置
OV5640的DCMI输出是行场同步信号(VSYNC、HSYNC)+像素时钟(PCLK)+数据(D0-D7),一开始我用STM32CubeMX配置DCMI时,把PCLK的极性设反了,采出来的图像全是花的。后来翻了OV5640的 datasheet,才知道PCLK是上升沿有效,VSYNC是高电平有效,HSYNC是高电平有效,改了配置后图像终于正常了。

踩坑2:SRAM不够用
一帧320×320的RGB888图像是307200字节,STM32H743的SRAM是1MB,本来够,但一开始我把DCMI的DMA缓冲区设到了ITCM里(速度快),结果ITCM只有64KB,根本存不下。后来把缓冲区设到了DTCM(128KB)+ AXI SRAM(512KB)里,用双缓冲模式——采第一帧的时候存在缓冲区1,采第二帧的时候存在缓冲区2,同时把缓冲区1的数据发给以太网,这样速度更快,也不会丢帧。


STM32裸机代码:重点是高效采集和稳定传输

STM32的代码我用的是STM32CubeMX生成的HAL库,重点写了三个部分:OV5640初始化+DCMI采集、图像预处理、W5500以太网通信。

1. OV5640初始化+DCMI采集

OV5640的初始化是通过I2C接口写寄存器,我直接用了网上开源的OV5640初始化代码,稍微改了改输出格式——设成RGB565,分辨率320×240(后来预处理再Resize到320×320,这样采集速度更快)。

DCMI采集用的是DMA双缓冲模式,代码大概是这样的:

// 定义双缓冲区,放在AXI SRAM里
#define BUFFER_SIZE (320 * 240 * 2) // RGB565是2字节 per pixel
uint8_t buffer1[BUFFER_SIZE] __attribute__((section(".AXISRAM")));
uint8_t buffer2[BUFFER_SIZE] __attribute__((section(".AXISRAM")));
volatile uint8_t dma_complete_flag = 0;
volatile uint8_t current_buffer = 0;

// DCMI DMA完成回调函数
void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmi)
{
dma_complete_flag = 1;
// 切换缓冲区
current_buffer = !current_buffer;
// 启动下一次采集
HAL_DCMI_Start_DMA(hdcmi, DCMI_MODE_CONTINUOUS,
(uint32_t)(current_buffer ? buffer2 : buffer1), BUFFER_SIZE / 2);
}

// 主循环里的采集逻辑
while (1)
{
if (dma_complete_flag)
{
dma_complete_flag = 0;
uint8_t *img_buffer = current_buffer ? buffer1 : buffer2;
// 预处理图像
PreprocessImage(img_buffer, 320, 240);
// 发送给上位机
SendToPC(preprocessed_buffer, 320 * 320 * 3);
}
}

2. 图像预处理:最近邻插值Resize,省CPU

预处理要做两件事:RGB565转RGB888和320×240 Resize到320×320。

RGB565转RGB888很简单,就是把5位的R、6位的G、5位的B分别左移到8位:

uint16_t rgb565 = (img_buffer[i*2] << 8) | img_buffer[i*2+1];
uint8_t r = (rgb565 >> 11) << 3;
uint8_t g = ((rgb565 >> 5) & 0x3F) << 2;
uint8_t b = (rgb565 & 0x1F) << 3;

Resize我用了最近邻插值,因为双线性插值要算四个像素的平均值,太费CPU,最近邻插值直接取最近的像素,速度快,精度损失也不大——320×240到320×320,就是把上下各补40行黑色像素,或者把图像拉伸,我选了拉伸,代码大概是这样的:

// 预处理后的缓冲区,320×320 RGB888
uint8_t preprocessed_buffer[320 * 320 * 3] __attribute__((section(".AXISRAM")));

void PreprocessImage(uint8_t *src, int src_w, int src_h)
{
int dst_w = 320, dst_h = 320;
for (int y = 0; y < dst_h; y++)
{
// 最近邻插值:src_y = y * src_h / dst_h
int src_y = y * src_h / dst_h;
for (int x = 0; x < dst_w; x++)
{
int src_x = x * src_w / dst_w;
int src_idx = (src_y * src_w + src_x) * 2;
int dst_idx = (y * dst_w + x) * 3;

// RGB565转RGB888
uint16_t rgb565 = (src[src_idx] << 8) | src[src_idx+1];
preprocessed_buffer[dst_idx] = (rgb565 >> 11) << 3;
preprocessed_buffer[dst_idx+1] = ((rgb565 >> 5) & 0x3F) << 2;
preprocessed_buffer[dst_idx+2] = (rgb565 & 0x1F) << 3;
}
}
}

3. W5500以太网通信:加帧头帧尾,避免丢包

W5500的驱动我用的是官方的ioLibrary_Driver,稍微改了改,用TCP Server模式,等待C#上位机连接。

通信协议我自己定了一个简单的帧格式,避免丢包和乱序:

帧头(2字节)数据长度(4字节)图像数据(N字节)校验和(1字节)帧尾(2字节)
0xAA 0xBB N = 3203203 预处理后的RGB888数据 所有数据的异或和 0xCC 0xDD

发送代码大概是这样的:

void SendToPC(uint8_t *data, int len)
{
// 帧头
uint8_t header[] = {0xAA, 0xBB};
// 数据长度(大端序)
uint8_t len_bytes[] = { (len >> 24) & 0xFF, (len >> 16) & 0xFF,
(len >> 8) & 0xFF, len & 0xFF };
// 校验和
uint8_t checksum = 0;
for (int i = 0; i < len; i++) checksum ^= data[i];
// 帧尾
uint8_t tail[] = {0xCC, 0xDD};

// 发送
send(socket, header, sizeof(header), 0);
send(socket, len_bytes, sizeof(len_bytes), 0);
send(socket, data, len, 0);
send(socket, &checksum, 1, 0);
send(socket, tail, sizeof(tail), 0);
}

踩坑:W5500的SPI速度
一开始我把W5500的SPI时钟设到了50MHz,结果通信经常出错,后来降到25MHz,就稳定了——W5500的最大SPI时钟是80MHz,但杜邦线连接的话,信号衰减大,还是设低一点好。


C#上位机:重点是网络重组帧和TensorRT推理

C#上位机的代码就简单多了,用之前YOLOv8的代码改改就行,重点加了网络通信和帧重组。

1. 网络通信:TcpClient接收数据,重组帧

用System.Net.Sockets.TcpClient连接STM32的W5500,然后用一个后台线程不断接收数据,重组帧。

帧重组的逻辑是:先找帧头0xAA 0xBB,然后读4字节的数据长度,再读对应长度的图像数据,然后读校验和,最后找帧尾0xCC 0xDD,校验和对的话,就把图像数据交给推理线程。

代码大概是这样的:

private TcpClient _tcpClient;
private NetworkStream _stream;
private ConcurrentQueue<byte[]> _imageQueue = new ConcurrentQueue<byte[]>();

// 后台线程接收数据
private async Task ReceiveDataAsync(CancellationToken cancellationToken)
{
var buffer = new byte[1024 * 1024]; // 1MB缓冲区
int bufferIndex = 0;
while (!cancellationToken.IsCancellationRequested)
{
if (_stream.DataAvailable)
{
int bytesRead = await _stream.ReadAsync(buffer, bufferIndex, buffer.Length bufferIndex, cancellationToken);
bufferIndex += bytesRead;

// 尝试重组帧
while (bufferIndex >= 2 + 4 + 1 + 2) // 至少要帧头+长度+校验和+帧尾
{
// 找帧头
int headerIndex = FindHeader(buffer, bufferIndex, new byte[] { 0xAA, 0xBB });
if (headerIndex == 1)
{
// 没找到帧头,清空缓冲区
bufferIndex = 0;
break;
}
// 把帧头移到缓冲区开头
Array.Copy(buffer, headerIndex, buffer, 0, bufferIndex headerIndex);
bufferIndex -= headerIndex;

// 读数据长度
int dataLen = (buffer[2] << 24) | (buffer[3] << 16) | (buffer[4] << 8) | buffer[5];
if (bufferIndex < 2 + 4 + dataLen + 1 + 2)
{
// 数据不够,继续接收
break;
}

// 读图像数据
var imageData = new byte[dataLen];
Array.Copy(buffer, 6, imageData, 0, dataLen);
// 读校验和
byte checksum = buffer[6 + dataLen];
// 读帧尾
var tail = new byte[] { buffer[6 + dataLen + 1], buffer[6 + dataLen + 2] };

// 校验
byte calculatedChecksum = 0;
foreach (var b in imageData) calculatedChecksum ^= b;
if (calculatedChecksum == checksum && tail.SequenceEqual(new byte[] { 0xCC, 0xDD }))
{
// 校验通过,加入队列
_imageQueue.Enqueue(imageData);
}

// 移除已处理的数据
int processedLen = 2 + 4 + dataLen + 1 + 2;
Array.Copy(buffer, processedLen, buffer, 0, bufferIndex processedLen);
bufferIndex -= processedLen;
}
}
await Task.Delay(10, cancellationToken);
}
}

// 找帧头的辅助方法
private int FindHeader(byte[] buffer, int bufferLen, byte[] header)
{
for (int i = 0; i <= bufferLen header.Length; i++)
{
bool found = true;
for (int j = 0; j < header.Length; j++)
{
if (buffer[i + j] != header[j])
{
found = false;
break;
}
}
if (found) return i;
}
return 1;
}

2. YOLOv8推理:用之前的TensorRT代码,改改输入尺寸

推理部分和之前的文章一样,就是把输入尺寸改成320×320,模型换成YOLOv8n INT8 320×320的engine,速度更快——实测一帧推理只要10ms左右。

推理完成后,把检测结果(合格/不合格、缺陷类型)发回给STM32,STM32控制LED灯亮。


最后看看效果:成本砍到1/10,速度还不慢

在实验室里跑了一周,测试数据如下:

  • 硬件成本:STM32H7+OV5640+W5500一套200元,之前的T400工控机一套2000元,成本砍到1/10;
  • 总延迟:STM32采集+预处理+传输50ms,上位机推理+后处理10ms,总延迟60ms;
  • 精度:用320×320的YOLOv8n INT8,mAP只比640×640的FP16掉了0.02,完全满足现场巡检的需求;
  • 稳定性:连续跑了7天,没丢过一帧,没出过一次错。

李老板看完测试数据,直接拍板订了100套硬件,说这个月就能把便携式设备做出来。其实做嵌入式部署的核心思路就是:别硬让单片机干自己不擅长的事,分工协作,把采集、推理、控制分开,成本和速度都能兼顾。

大家如果在做C# YOLOv8的嵌入式部署,不妨试试这套方案,有什么问题欢迎一起交流。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 成本砍到1/10!威哥STM32H7+OV5640+C# YOLOv8边缘推理实战:裸机协同上位机毫秒级检测
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!