
最近不少做嵌入式视觉的朋友在问怎么把OpenMV跟STM32玩起来正好我手上刚完成了一个车牌识别系统的小项目整套链路是OpenMV采集图像、STM32做控制联动、PC端用YOLOv11检测车牌区域、PaddleOCR识别文字。很多初学者一听“车牌识别”就以为要上树莓派或者Jetson其实用OpenMVSTM32这套轻量组合也能搭出能跑的原型而且工程思路特别适合做毕业设计或者入门嵌入式AI。这篇我把从硬件接线、固件配置、模型训练到OCR部署的完整过程都写出来包括我踩过的坑和换过三次方案的教训希望能帮你少走弯路。这个项目适合三类人正在做嵌入式相关课设或毕设的学生、想熟悉OpenMV与STM32通信的开发爱好者、以及想把深度学习检测模型和单片机结合起来做落地demo的工程师。你不需要有很强的算法基础但建议先会点MicroPython、熟悉STM32的基本外设编程再来看这篇文章会顺畅很多。整套流程我尽量按“能跑起来”的标准写每个环节都给到可直接用的配置和代码片段。1. 车牌识别系统的整体设计与方案选型1.1 为什么选OpenMVSTM32而不是单板电脑先说个很多人纠结的问题车牌识别这个任务OpenMV的算力明显不够直接在板子上跑YOLOv11STM32更跑不动深度学习推理那为什么还要用这套组合我的答案是——你做的不只是一个识别器而是一个完整的嵌入式视觉系统。OpenMV的优势在于它把摄像头、图像处理、IO控制集成在一块小板上MicroPython环境上手快可以快速验证图像采集、颜色识别、串口通信这些底层逻辑。STM32的优势在于外设丰富、实时性强适合做道闸控制、LED提示、传感器联动而且它是很多嵌入式岗位的基本功。把两者结合起来正好发挥各自的长处这也是工业上常见的主控视觉模组架构。如果你直接上树莓派或者RK3588这类板子确实跑算法很爽但成本和体积都上去了而且失去了“自己做主控逻辑”的锻炼机会。从学习角度讲先在小系统上把数据流跑通再迁移到大算力平台思路会清晰很多。1.2 我的系统架构和它解决了什么问题这套车牌识别系统整体分三层图像采集层OpenMV负责实时拍摄画面检测到运动或者触发信号后把当前帧压缩成JPEG通过串口发给PC端同时也通过IO口通知STM32“正在处理”。控制决策层STM32作为主控接收OpenMV的状态信号控制舵机模拟道闸、蜂鸣器和LED指示灯并把状态信息显示在OLED屏上。云端或PC识别层PC端接收OpenMV传来的图像先用YOLOv11做目标检测框出车牌区域再用PaddleOCR对裁剪后的区域做文字识别最后把结果通过串口回传给STM32显示。这套架构的好处是各模块职责单一、容易调试。哪怕任何一个环节出问题都能单独验证。比把所有逻辑塞进一块板子里要清晰得多也符合真实产品的分层思路。1.3 硬件清单和拓扑关系我在实际项目中用到的硬件如下硬件型号/规格用途视觉模组OpenMV Cam H7 Plus图像采集与预处理主控STM32F407ZGT6开发板控制逻辑与联动摄像头MT9M114OpenMV自带拍摄车牌画面舵机SG90模拟道闸抬杆动作显示器0.96寸OLED I2C显示识别结果报警有源蜂鸣器 绿色/红色LED识别成功/失败提示USB转TTLCH340模块OpenMV与STM32串口调试及PC通信电源5V 3A适配器 AMS1117降压模块供电通信拓扑上OpenMV的P1TX、P0RX分别接STM32的USART2_RX和USART2_TX波特率设115200。PC端识别时STM32的USART1再引出到CH340模块跟电脑连形成OpenMV→STM32→PC这样一条数据链路。上一版方案我用过OpenMV直接连PC少了主控中转虽然也能跑但就失去了单片机参与联动的意义最后还是按现在的结构重新搭了。2. OpenMV端图像采集、检测触发与串口通信细节2.1 OpenMV固件选择与初始配置OpenMV官方固件通常够用但如果你需要跑一些额外的神经网络模型建议使用带IDE最新版固件更新方法很简单用USB连接OpenMV到电脑打开OpenMV IDE点击“工具→固件更新”勾选“选择最新版本”即可。我建议定期更新因为我曾经在旧固件上遇到串口DMA相关的奇怪Bug升级之后就好了。OpenMV上的存储空间不大H7 Plus是16MB Flash如果后续要存放神经网络模型或较多图片可以在SD卡槽插一张32GB的TF卡并创建一个images目录用于存放抓拍的车牌图片。下面是我在OpenMV端用的初始化代码片段包含串口和摄像头基础配置import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240兼顾清晰度与传输速度 sensor.skip_frames(time2000) sensor.set_auto_whitebal(True) # 串口初始化P1 TX, P0 RX对应STM32的USART2 uart UART(3, 115200, timeout_char1000) uart.init(115200, bits8, parityNone, stop1) # 定义一个简单的帧协议帧头数据长度数据 FRAME_HEADER b\xA5\x5A def send_frame(img_bytes): length len(img_bytes).to_bytes(2, big) uart.write(FRAME_HEADER length img_bytes) print(sent frame, size , len(img_bytes))这里我特意把分辨率设在QVGA因为再高的分辨率串口传输延迟会明显变大。实测在115200波特率下一张QVGA的JPEG图大概15~25KB传输时间约1.5秒做原型完全够用。2.2 触发抓拍策略连续帧差异检测车牌识别不需要每一帧都传否则串口会被塞爆。我采用的是“画面差异检测”方式OpenMV持续采集画面当检测到画面中有明显变化比如车辆驶入才抓拍一帧并发送。这样既降低了通信压力也能减少PC端无效识别。差异检测的基础是计算前后两帧对应像素的平均绝对值差超过阈值就认为画面有变化。OpenMV提供了image.get_similarity()方法但更轻量的是用帧差法。下面是我的实现previous_frame sensor.snapshot().copy() threshold 25.0 while True: current_frame sensor.snapshot() diff current_frame.get_similarity(previous_frame) # get_similarity返回0~11表示完全不同 if diff 0.35: print(motion detected, similarity diff , diff) img_bytes current_frame.compressed(quality70) send_frame(img_bytes) time.sleep(1) # 避免连续抓拍导致串口阻塞 previous_frame current_frame.copy()阈值0.35是我在室内灯光环境下调出来的如果你在室外强光环境可能需要适当提高到0.5左右。注意time.sleep(1)很关键——它防止同一辆车在画面里连续触发多次抓拍虽然会漏掉一些帧但对车牌识别场景来说足够了。2.3 OpenMV串口通信协议设计要避开的坑单片机之间通信最怕数据错位。因为图像数据是二进制流如果接收端不知道一帧从哪里开始到哪里结束很容易把上一帧尾部当成下一帧头部。我采用的协议很简单0xA5 0x5A作为两字节帧头紧接着两个字节表示数据长度大端序后面跟的是JPEG图像字节流。STM32端解析时先等帧头然后读长度再按长度收满整帧数据。这个协议没有加校验和实际用下来误码率极低如果你对稳定性要求更高可以在帧尾加一个CRC16或简单的异或校验。我踩过的一个坑是OpenMV的uart.write()如果一次写入大量数据偶尔会被系统中断切成两段接收端如果不做缓冲处理就会解析失败。解决办法是把发送函数改成循环发送并加小延时或者在STM32端用DMA空闲中断接收细节见下一节。3. STM32端主控驱动、串口接收与OLED显示3.1 STM32开发环境搭建要点含C51兼容提示很多人在STM32开发环境上卡了很久尤其是刚从51单片机转过来的同学经常被Keil的芯片包和编译器版本搞得头大。我用的是Keil MDK 5.37版本安装STM32F4系列支持包时要注意如果你以前装过C51版Keil建议把两个版本分开装在不同目录否则UV4.exe会冲突。打开Keil后进入Pack Installer搜索STM32F4xx_DFP下载1.2.2或更高版本。如果下载速度慢可以手动去Keil官网下载DFP包然后双击安装。STM32的USB无法识别问题90%是驱动没装好。建议下载STM32CubeProgrammer或STM32 ST-LINK Utility附带驱动安装后重新插线。注意有些开发板用的是一键下载电路如正点原子、野火的板子需要先装CH340或CP2102的USB转串口驱动。工程创建方面我建议直接用STM32CubeMX生成初始化代码勾选USART1、USART2、I2C1、TIM3等外设时钟配置为外部8MHz晶振倍频到168MHz生成后再在Keil里添加自己的业务逻辑代码。这样的好处是避免手写寄存器配置出错并且代码可读性高。3.2 STM32串口中断接收与环形缓冲区STM32这边最关键的是串口接收图像帧。由于图像数据比较大用简单的阻塞式接收会浪费CPU而且容易丢字节。我的做法是用USART2接收OpenMV的数据开启接收中断每收到一个字节就放入环形缓冲区在主循环里解析缓冲区寻找帧头、读取长度、提取图像数据再通过USART1转发给PC。环形缓冲区的实现不复杂但要注意读写指针的同步。我直接用了一个256字节的结构体#define RX_BUFFER_SIZE 2048 typedef struct { uint8_t buffer[RX_BUFFER_SIZE]; volatile uint16_t head; volatile uint16_t tail; } RingBuffer;中断服务函数里void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART2); uint16_t next_head (ring.head 1) % RX_BUFFER_SIZE; if (next_head ! ring.tail) { ring.buffer[ring.head] data; ring.head next_head; } // 若缓冲区满则丢弃当前字节避免覆盖未读数据 } }主循环解析时要先判断缓冲区里是否有完整的一帧数据再处理。核心逻辑是状态机分为“找帧头0xA5”、“验证0x5A”、“读长度高字节”、“读长度低字节”、“收数据体”五个状态。我用这个方式跑了半小时图像流亲测不会丢帧。3.3 OLED显示和舵机控制联动识别结果通过OLED显示我用的是0.96寸I2C接口SSD1306驱动。在STM32的I2C1上SCL接PB6SDA接PB7。实际使用中需要注意I2C上拉电阻部分开发板没有板载上拉需要外接4.7kΩ电阻到3.3V否则OLED不显示。显示代码就是标准SSD1306库核心是封装一个OLED_ShowString()在接收到PC端回传的识别字符串后把内容打印到屏幕上。我在屏幕上分了三行第一行显示当前状态“DETECTING”还是“RESULT”第二行显示车牌号第三行显示识别置信度或时间信息。舵机控制我用TIM3输出PWM频率50Hz占空比5%~10%对应0°~180°。识别成功后STM32把舵机转到90°模拟抬杆蜂鸣器响一声识别失败则保持0°不抬杆红灯亮。这个联动逻辑很简单但很直观能让人一眼看懂系统的工作状态。4. PC端识别YOLOv11车牌检测与PaddleOCR文字识别完整配置4.1 环境准备Python、CUDA与依赖安装PC端识别是整个系统的智能大脑。我的环境是Windows 11 Python 3.10 CUDA 12.1 PyTorch 2.2。装好Python后建议创建独立的虚拟环境conda create -n plate python3.10 -y conda activate plate pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121为什么强调CUDA版本因为PaddleOCR的GPU版也有对应的CUDA要求如果混用版本paddle的CUDA算子经常会报错。我用的PaddlePaddle GPU版安装命令如下python -m pip install paddlepaddle-gpu2.6.1.post112 -f https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html如果你电脑没有NVIDIA显卡也可以直接装CPU版pip install paddlepaddle但CPU版跑PaddleOCR的识别速度会慢很多每张图大概多花0.5~1秒做实时性要求高的场景就很吃力。我强烈建议有显卡的同学直接上GPU版速度差距是数量级的。接下来安装YOLOv11依赖和PaddleOCRpip install ultralytics paddleocr paddlepaddle这里提醒一下PaddleOCR 3.x版本的接口和2.x有一些变化尤其是predict方法和结果数据结构。如果搜网上的旧教程很多写法会报错建议以官方文档为准。4.2 YOLOv11环境配置与车牌数据集准备YOLOv11是Ultralytics出的新模型检测精度和速度都比较均衡。安装好ultralytics库后你需要准备车牌检测数据集。我把自己标注的800张车牌图片整理成YOLO格式目录结构如下plate_dataset/ images/ train/ val/ labels/ train/ val/每张图片对应一个同名txt标签文件格式是class x_center y_center width height坐标是归一化到0~1的。标注工具我用的是LabelImg画框后自动生成YOLO格式。车牌区域通常是一个横着的矩形标注时尽量贴合车牌边缘不要留太多白边否则会影响PaddleOCR的识别精度。数据集准备好后创建一个plate.yamlpath: D:/projects/plate_dataset train: images/train val: images/val nc: 1 names: [license_plate]注意path最好用绝对路径有时候相对路径会因为工作目录变化而出错。接下来开始训练yolo detect train dataplate.yaml modelyolov11n.pt epochs200 imgsz640 batch16 device0我用的YOLOv11n是轻量版训练速度快在一张RTX 3060上200个epoch大概1小时左右。如果你追求更高精度可以换yolov11s或yolov11m但推理速度会下降。4.3 训练好的模型做预测并保存裁剪车牌图YOLOv11预测有两种场景单张图片处理和摄像头实时流。我这里因为OpenMV会不断传图过来所以主要做单张图片处理用predict接口拿到检测框然后把框里的区域裁剪出来保存成单独图片再传给PaddleOCR。以下是核心代码from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) result model.predict(captured.jpg, conf0.35, imgsz640, verboseFalse) boxes result[0].boxes.xyxy.cpu().numpy() if len(boxes) 0: print(no plate detected) else: for idx, box in enumerate(boxes): x1, y1, x2, y2 [int(v) for v in box] crop result[0].orig_img[y1:y2, x1:x2] cv2.imwrite(fplate_crop_{idx}.jpg, crop)实测中我发现修剪出的车牌区域如果倾斜角度太大OCR识别率会明显下降。所以建议在裁剪前对检测框做一个轻微的角度矫正最简单的方式是用OpenCV的minAreaRect加仿射变换虽然会增加一些计算量但收益很值。4.4 PaddleOCR 3.x安装与文字识别乱码排查PaddleOCR在3.x版本中把OCR能力整合为PaddleOCR类初始化参数与2.x基本一致。我的OCR初始化代码如下from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse)识别函数result ocr.predict(plate_crop_0.jpg) # 3.x的返回结果是列表需要逐层取数据 for res in result: for line in res: text line[rec_texts][0] score line[rec_scores][0] print(text, score)这里有个3.x版本特有的坑ocr.ocr()在2.x中返回的是嵌套列表3.x改成了predict()而且返回格式变了。如果你用了网上旧教程的result[0][0][1][0]那种索引方式很可能会报KeyError。建议打印result的结构看看。关于乱码问题我遇到的情况是中文车牌字符显示成方块或问号主要原因有三个中文字体缺失PaddleOCR识别结果在终端打印没问题但保存到文件后用Excel打开乱码这是编码问题保存时强制用encodingutf-8-sig即可。图片分辨率太低车牌区域裁剪后太小OCR识别字母数字可以但中文字符会混淆。我的经验是裁剪后的车牌宽度最好大于150像素如果小于100先做双线性插值放大到2倍再识别。识别模型把省份简称认错这是模型本身精度问题可以通过增大YOLO检测框的置信度阈值来过滤低质量检测只对高置信度图做OCR。我最终在测试集上跑了效果YOLOv11检测mAP50约0.93PaddleOCR文字识别准确率约0.88整体端到端识别成功率约0.82。识别失败主要集中在夜间低照度和车牌倾斜角度过大的情况。5. 端到端联调OpenMV、STM32、PC三方协同处理5.1 完整数据流向把三端串起来后数据流向是这样的OpenMV上电初始化持续拍摄并计算帧差异。检测到画面变化后OpenMV抓拍JPEG图片并通过串口发送给STM32。STM32收到完整帧后通过另一路串口连接CH340把图片字节原样转发给PC端。PC端Python程序监听串口收到图片后先调用YOLOv11检测车牌区域再调用PaddleOCR识别文字。PC端把识别结果字符串通过串口回传给STM32。STM32解析结果后控制OLED显示、舵机和蜂鸣器。这套数据流看起来长但因为每步都很清晰调试时能快速定位是哪个环节出问题。我最开始尝试在STM32上直接跑图像算法发现死路一条后来把识别上移到PC端整条链路才跑通。5.2 PC端串口监听与图像接收Python代码PC端用Python的pyserial库监听串口。波特率要和STM32转发端一致我用的115200建议加一个超时保护避免串口卡死导致线程阻塞以下是核心代码import serial import cv2 import numpy as np ser serial.Serial(COM5, 115200, timeout1) buffer bytearray() def extract_frame(byte_stream): # 在字节流中寻找帧头0xA5 0x5A并提取完整帧 while True: idx byte_stream.find(b\xA5\x5A) if idx -1: byte_stream.clear() return None if len(byte_stream) - idx 4: break length int.from_bytes(byte_stream[idx2:idx4], big) if len(byte_stream) - idx 4 length: frame bytes(byte_stream[idx4:idx4length]) del byte_stream[:idx4length] return frame break if idx 0: del byte_stream[:idx] return None while True: data ser.read(256) if data: buffer.extend(data) frame_bytes extract_frame(buffer) if frame_bytes: img cv2.imdecode(np.frombuffer(frame_bytes, np.uint8), cv2.IMREAD_COLOR) if img is not None: cv2.imwrite(captured.jpg, img) # 调用YOLO和OCR再把结果回传这段代码里最关键的是extract_frame函数。它不只是找一次帧头而是要在字节流中循环查找防止出现“帧头前面残留了上一个帧的尾部垃圾数据”的情况。这个逻辑我调试了整整一个下午才跑顺。5.3 识别结果如何回传STM32并联动外设PC端识别完成后生成一个类似京A12345的字符串通过串口发给STM32。为了保险起见我发之前会做一个简单的加密处理其实就是加一个头尾标记让STM32能确认指令完整性。实际发送格式如下0xAA 0x55 结果长度 结果ASCII 0x0D 0x0ASTM32端在收到完整指令后从缓冲区解析出结果字符串再调用OLED_ShowString()显示。同时判断结果是否包含中文省份缩写如“京”“沪”等如果包含说明检测成功转动舵机否则说明只检测到了字母数字或者没有检测到结果蜂鸣器长鸣两声提示失败。这里有一个细节UTF-8和GBK编码问题STM32串口收到的是UTF-8字节OLED库本身只支持ASCII字符显示中文字符会显示成乱码。我的解决方法是只在OLED上显示字母数字部分中文省份用拼音首字母替代比如“京A12345”显示成“JING A12345”这样虽然不完美但至少用户能看懂。如果需要完整显示中文就得在STM32里内置中文字库用取模软件把需要用到的汉字转成点阵数据再自己写一个OLED_ShowCHinese()函数。这个我后面加了几个常用省份字进去效果还不错但工程量上来了非必要不建议一开始就做。6. 常见问题与排查技巧实录联调过程中我记录了不少典型问题这里整理成表格方便你对照排查。现象可能原因解决办法OpenMV串口发送后STM32收不到波特率不一致TX/RX接反检查两端波特率交叉连接TX→RXSTM32收到乱码或残缺帧串口接收未用中断缓冲区太小改DMA空闲中断环形缓冲区不少于2KBOpenMV画面异常偏亮/偏暗白平衡和曝光未固定关闭自动增益手动设置曝光值YOLOv11检测不到车牌训练数据太少置信度阈值过高增加数据量conf降到0.25~0.3PaddleOCR识别结果乱码图片分辨率太低编码问题放大裁剪图保存文件时用utf-8-sigPC端串口读取阻塞卡死没有超时设置线程同步问题serial.Timeout设为1用独立线程读取OLED不显示I2C地址错误上拉电阻缺失扫描地址是否0x3C外接4.7kΩ上拉舵机抖动或无法转到位PWM频率不对电压不足设置50Hz频率单独给舵机供电6.1 YOLOv11小目标优化的几个实测技巧车牌在整张画面里通常只占很小比例YOLOv11虽然比旧版对小目标更友好但直接拿默认参数跑还是容易漏检。我做了几个优化效果立竿见影把imgsz从640提到960。虽然推理时间变长但车牌区域在特征图上的像素点数变多检测召回率明显提升。数据增强时增加mosaic1.0和mixup0.2。这样模型能学到更多背景变化不会只认某种固定场景下的车牌。后处理时把conf设为0.3、iou设为0.45。如果设太高比如0.5低质量检框会被滤掉漏检率上升。另外如果你的车牌图片存在严重倾斜可以在YOLO训练数据中额外加入一些旋转增强或者在预测后对检测框区域单独做透视矫正再送OCR。倾斜角度超过15度时检测框虽然能框住但OCR识别率会断崖式下跌。6.2 PaddleOCR识别速度与“乱码”问题深度剖析PaddleOCR的乱码问题其实分成“显示乱码”和“识别结果错乱”两种。显示乱码基本都是编码问题你只需要注意Python写入文件时用utf-8-sig以及读取时统一编码格式即可。真正头疼的是识别结果错乱比如“京”被识别成“示”或者“就”这跟模型在中文车牌数据上的表现有关。我试过几种优化一是把裁剪出来的车牌图片做二值化增强文字对比度二是先做一个简单的HSV颜色分析如果是蓝底白字车牌可以用蓝色通道掩膜把底牌区域提出来再OCR三是用use_angle_clsTrue加方向分类器对横竖车牌都能自动纠正方向。综合这三种处理后我的识别准确率从0.82提升到了0.87左右。6.3 关于STM32与OpenMV的供电和稳定性这个坑很多人会忽略OpenMV和STM32如果共用同一个USB口供电电流时常不够会导致OpenMV采集图像时突然重启或者STM32的串口丢数据。我的做法是给OpenMV单独接5V供电STM32单独用USB或者外部电源共地后再通信。地线必须连在一起否则串口的电平参考不一致数据全是乱码。如果你在工业现场使用建议增加光耦隔离或者RS485收发器来延长通信距离同时抑制干扰。家用实验环境直接用TTL串口就行线材尽量短一些不要超过20cm否则高频图像数据很容易被干扰。7. 一些来自实操的经验建议整个项目从零开始到我跑通前前后后花了两周多时间踩的坑几乎比写的代码还多。最后分享几个我真实体会比较深的建议。第一先分别测试每个模块再联调。OpenMV自己先能拍到照片、能发串口STM32自己先能收固定数据、能驱动OLED和舵机PC端自己先能用单张图片跑通YOLO和OCR然后再把它们串起来。模块都没验证就联调出了问题根本不知道从哪查起。第二做毕业设计的话不用追求识别率特别高重点是把整个流程跑通并表现出每个环节的技术理解。答辩时老师更关心你懂不懂为什么用YOLOv11、为什么用PaddleOCR、串口协议怎么设计的而不是你刷到了多高的准确率。第三这套架构里所有组件都有替代方案。OpenMV可以换成K210或者ESP32-CAMSTM32可以换成GD32PaddleOCR也可以换成Tesseract或自训练CRNNYOLOv11可以换成YOLOv8或RT-DETR。理解了整个数据流的逻辑换掉任何一个组件都只是重写对应模块而已。这才是做这个项目最有价值的收获。最后再提一个扩展方向下次如果要做实时识别可以考虑把PC端改成局域网传输OpenMV通过WiFi模块把图片发到服务器识别STM32只做云端指令的下发执行这样就能在硬件不变的前提下显著提升整体响应速度。这个思路我已经在规划了跑通了再来更新。