OpenMV+STM32+PC车牌识别实战:YOLOv11与PaddleOCR应用
发布时间:2026/9/28 13:49:57来源:尧图网络
1. 系统架构拆解OpenMV、STM32与PC端各自该干什么很多人看到这个标题的第一反应是OpenMV那块板子跑得动YOLOv11PaddleOCR这种OCR引擎是不是也得塞进STM32里这里先泼一盆冷水——OpenMV的处理器是STM32H7或者STM32F4系列算力天花板摆在那里跑MicroPython环境别说YOLOv11就是跑YOLOv2的tiny版本都费劲。PaddleOCR更不用想它需要完整的PaddlePaddle推理框架支撑不是单片机层面能干的事。所以这个系统的正确架构是这样的PC端/边缘主机负责跑YOLOv11做车牌检测跑PaddleOCR做车牌字符识别是整个系统的“大脑”。OpenMV作为前端视觉采集模块做车牌的粗定位、颜色筛选、触发拍照同时也充当PC和STM32之间的数据桥接层。它不跑深度学习模型但可以用传统图像算法辅助检测把“车来了”这个事件尽早感知到。STM32作为执行控制核心解析OpenMV或上位机串口发来的数据帧控制道闸升降、显示屏亮字、语音播报、继电器开锁、LED灯指示等还可以做车辆测速、地感线圈联动之类的辅助功能。为什么要这么拆因为车牌识别系统的核心链路是图像采集 → 车牌定位 → 字符分割/识别 → 结果输出 → 执行控制。前两步里采集和粗定位对实时性要求高OpenMV的图像处理能力刚好够精确检测和文字识别对算力要求高必须交给PC最后的执行控制对确定性、响应速度要求高这是STM32的强项。这套架构还有一个现实理由——成本。一套带GPU的PC加一个OpenMV加一块STM32最小系统板总共也就几百到一千出头的硬件成本却能完成纯工业方案动辄几千上万的识别和闸机控制功能非常适合毕设、竞赛原型、小规模停车场改造这类场景。这里要特别纠正一个误区很多人觉得“OpenMVSTM32”就必须是完全离线的单片机方案。实际上对于车牌识别这种涉及深度学习模型的系统合理的方式是“本地视觉预处理 上位机深度学习识别 单片机执行控制”三个模块各管一段通信走串口就能把整个系统串起来。如果你非要把识别也做进OpenMV里那只能退回到OpenMV的Haar Cascade、模板匹配、颜色识别这种传统视觉方案能够识别固定格式的蓝底白字车牌部分字符但准确率和泛化能力都远不如YOLOv11PaddleOCR适用面窄很多。我的建议是传统视觉算法做前端触发深度学习模型做精确识别两者结合而不是二选一。后面每个模块会详细讲怎么实现。2. 硬件清单与接线方案一套能跑通的最小配置先把推荐配置列给你没必要一上来就上工业级硬件最小系统板加少量外围器件就足够实验和演示。模块推荐型号作用参考价格视觉采集OpenMV Cam H7 Plus / R6图像采集、车牌粗定位、串口通信300-600元主控STM32F103C8T6最小系统板执行控制、串口解析、外设驱动15-30元上位机任意Windows/Linux PC有NVIDIA GPU更好运行YOLOv11和PaddleOCR已有则不计摄像头OpenMV自带OV5640或USB摄像头接PC采集图像随OpenMV附赠舵机MG996R 或 SG90模拟道闸开合/摄像头角度调节10-30元继电器模块低电平触发单路继电器控制电锁、LED灯带、220V道闸5-15元显示屏SSD1306 OLEDI2C实时显示识别车牌号15-25元供电12V/2A适配器 AMS1117-3.3 LM2596降压模块给舵机、STM32、OpenMV供电20-40元这里有个特别容易踩的坑共地与电平匹配。OpenMV的串口UART是3.3V TTL电平STM32F103的引脚也是3.3V TTL两者直接连接TX/RX没问题前提是共地——就是两者的GND必须接在一起。如果你用了5V供电的USB转TTL模块去接STM32那就要小心了很多USB转TTL模块的TX引脚是5V电平直连STM32的RX脚有烧引脚的风险。推荐的接线方案OpenMV的P4UART3 TX→ STM32的PA10USART1 RXOpenMV的P5UART3 RX→ STM32的PA9USART1 TX两者GND互连STM32的PA1→ 舵机信号线舵机电源接5V注意舵机地线必须和STM32共地STM32的PB12→ 继电器IN引脚OLED的SDA、SCL → STM32的PB7、PB6I2C1PC和OpenMV之间也要通信通常用USB线直接连接OpenMV在PC端显示为一个虚拟串口这样YOLOv11检测的结果就可以通过OpenMV转发给STM32。一条USB线同时解决了供电、程序烧录、串口通信三个问题实测最省心。关于“STM32测频法”这个词很多人在做车辆测速的时候会用到。原理很简单STM32的定时器工作在输入捕获模式在车辆经过两个地感线圈或者红外对射传感器时记录脉冲间隔从而算出车速。如果把这个测速数据和车牌识别结果结合起来就能实现“过车测速 车牌抓拍 道闸联动”的完整流程。相关热搜词里出现“stm32测频法”大概率也是要做这类综合项目接入到系统里并不难后面STM32端程序设计那一章我会给出一个简易实现思路。3. 开发环境搭建最容易卡住新手的两个环节这个项目卡人最多的地方不在算法而在环境配置。我按Keil和Python两个方向分开讲每一个都附上我自己实测过的步骤和踩坑记录。3.1 Keil 5安装STM32芯片包很多新手装完Keil 5打开后发现Device列表里没有STM32F103C8这是因为芯片支持包Device Family Pack没装。Keil 5的架构是IDE和芯片包分离的你必须先下载对应厂家型号的DFP包。具体步骤打开Keil 5点击菜单栏的Pack Installer图标。如果网络正常Pack Installer会联网加载包列表在搜索框输入STM32F1找到Keil::STM32F1xx_DFP点击Install按钮。如果Pack Installer加载缓慢或失败直接去Keil官网下载对应版本的DFP安装包手动安装。注意版本要和你的Keil版本匹配我用的Keil 5.38配STM32F1xx_DFP 2.4.1完全没问题。安装完成后新建工程在Device选择界面输入STM32F103C8能看到芯片就说明一切正常。这里再补一个常见问题“keil5兼容c51和stm32安装”。Keil 5的MDK-ARM只支持ARM内核芯片C51单片机用的是另一个工具链Keil C51。如果你要同时开发51和STM32需要分别安装MDK-ARM和C51版本并且最好把两个安装目录分开或者用各自独立的UV4环境。两个版本共用同一个UV4.exe是常见冲突点最稳妥的方式是装在两台机器或者用虚拟机隔离不过实测装在同一个目录的不同子目录下大多数情况下也能正常工作。3.2 Python环境与PaddleOCR GPU版安装PaddleOCR的安装坑密集度极高我把我实测通过的一套流程写在这里。先装Miniconda比Anaconda轻量然后创建独立环境conda create -n plate_env python3.10 -y conda activate plate_envGPU版PaddlePaddle的安装有个必须注意的匹配关系——PaddlePaddle版本和CUDA版本必须对应。以CUDA 11.8为例python -m pip install paddlepaddle-gpu2.6.1.post118 -f https://www.paddlepaddle.org.cn/whl/windows/mkl/avx/stable.html装完验证import paddle paddle.utils.run_check()如果输出PaddlePaddle is installed successfully!就说明GPU版本装好了。没有NVIDIA显卡的机器就装CPU版训练跑不了但PaddleOCR的推理CPU也能跑只是速度慢一些实测一张车牌图CPU大概0.3到0.5秒GPU能到0.05秒以内。接下来装PaddleOCR。注意现在PaddleOCR 3.x版本和2.x版本的API差异很大网上很多教程是2.x时代的写法直接套用会报错。3.x版本的安装和调用pip install paddleocrPaddleOCR 3.x的调用方式from paddleocr import PaddleOCR ocr PaddleOCR( use_textline_orientationTrue, langch ) result ocr.predict(车牌区域图片.jpg)这里有个细节PaddleOCR 3.x默认会把predict返回结果包在一个Result对象里打印出来能看到识别文本字段rec_texts。2.x版本用ocr.ocr(img_path, clsTrue)的方式已经废弃了如果你照着旧教程调试第一行就会报错。还有一个高频问题“安装paddleocr gpu版本失败”。常见原因有两个装了PaddleOCR但没装paddlepaddle-gpu导致PaddleOCR只能走CPU。paddlepaddle-gpu版本和CUDA不匹配比如你在CUDA 12.x机器上装了CUDA 11.8的包运行时直接报CUDA driver version is insufficient。解决办法就是查清楚自己机器的CUDA版本nvidia-smi看右上角然后去PaddlePaddle官网找对应的whl包安装。不必一味追求最新版本稳定能用才是第一位的。3.3 OpenMV IDE与固件更新OpenMV IDE在官网下载安装即可连接OpenMV后如果提示固件版本过低用IDE的Tools → Run Bootloader更新固件过程没什么坑。唯一要注意的是更新固件会清空OpenMV的Flash存储区如果你之前存过文件要先备份。另外OpenMV的USB虚拟串口在跑程序的时候会被占用如果你PC端的Python程序也要访问同一个串口必须先把OpenMV IDE断开连接否则串口被占用会报错。这个细节排错时特别容易忽略。4. YOLOv11车牌检测模型从数据集到可落地的模型YOLOv11是Ultralytics团队推出的目标检测模型延续了YOLO系列的结构优势在工业场景下部署方便程度极高——一个pip install ultralytics就能跑推理导出ONNX、TensorRT也都有现成命令。用于车牌检测它负责的任务是从画面里框出车牌位置为后面PaddleOCR提供裁剪后的车牌图片。4.1 数据集获取与标注车牌检测训练集常用开源数据集比如CCPD中国城市停车场数据集2万张图、CRPD中国路内停车数据集等。CCPD是安徽中科大的数据集里面标注了车牌的边框坐标文件格式是xxx-91-85_x1-y1_x2-y2_xxx.jpg可以从文件名直接解析出标注框但更适合的做法还是转成YOLO格式的txt标注文件。如果做小规模验证也可以自己拍几百张停车场的照片用LabelImg或者X-AnyLabeling标注每张图框出车牌矩形区域导出为YOLO格式。自己标的数据集和CCPD混着训练效果更好因为你的摄像头安装角度和CCPD原图有很大差异纯用开源数据集训练出来的模型在国内水牌、新能源绿牌、老旧泛黄车牌上表现会不稳定。数据格式目录结构如下datasets/plate/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── plate.yamlplate.yaml内容path: datasets/plate train: images/train val: images/val nc: 1 names: [license_plate]4.2 环境配置与训练命令pip install ultralytics然后开始训练yolo detect train \ datadatasets/plate/plate.yaml \ modelyolo11n.pt \ epochs100 \ imgsz640 \ batch8 \ device0 \ patience20GPU显存不够的话把model换成yolo11n.ptnano版本imgsz降到416batch降到2照样能跑只是精度会低一些。这里提一下相关热搜词里“yolov11小目标优化”这个方向。车牌在画面里的像素占比往往很小尤其摄像头装在道闸杆上方俯拍时30米开外的车拍下来车牌可能只有20x60像素属于典型的小目标检测。YOLOv11对8x8像素以上的特征图都有检测能力但极小目标仍然容易漏检。常见的优化手法切片推理SAHI把大图切块后再检测小目标在切片里相当于被放大了实测对小目标召回率提升非常明显。提高输入分辨率把imgsz从640提升到960模型能看到更多细节代价是推理速度变慢。数据增强在训练时对车牌区域做随机裁剪放大相当于让模型多见到“大车牌”的样本。多尺度训练开启mosaic1.0和scale0.5让模型适应不同尺度的车牌。实际项目中SAHI是最容易生效且不需要重新训练的优化方式我的习惯是先跑基线模型遇到漏检再加SAHI。4.3 推理结果保存与导出YOLOv11推理和保存结果from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcetest_images/, conf0.35, saveTrue, # 保存标注后的图片 save_txtTrue, # 保存txt格式的检测结果 save_confTrue, # 在txt里带上置信度 imgsz640 )如果你用Java后端做车牌识别平台需要把模型转成ONNX格式在Java端调用yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640导出后用onnxruntime在Java/Python里加载推理相关热搜词里“java onnx 车牌识别”就是这么一套流程。要注意导出的ONNX动态输入维度默认是1x3x640x640如果输入尺寸不固定需要设置dynamicTrue参数。另外“yolov11预测后保存”是高频搜索词实际就是上面这个saveTrue参数很多人不知道而已。每次训练和推理的结果会自动存到runs/detect/目录下按时间递增生成子文件夹不会覆盖旧结果这一点比一些深度学习框架默认覆盖的方式体贴很多。5. PaddleOCR车牌文字识别安装、调用与乱码排查YOLOv11检测出车牌后下一步就是识别车牌上的汉字、字母和数字。这里用PaddleOCR的文本识别能力注意检测模块det在这里并不用来做车牌定位因为车牌区域已经被YOLOv11框出来了PaddleOCR只做识别rec就够了。但实际Pipeline里PaddleOCR的文本检测模型偶尔也会被用于二次精修因为YOLOv11框出来的区域可能包含保险杠边缘、螺丝钉等干扰物用OCR的检测模型可以进一步把字符区域框出来。5.1 PaddleOCR 3.x完整识别流程把YOLOv11检测到并裁剪好的车牌区域图保存为某个目录下的图片然后调用PaddleOCRimport cv2 import numpy as np from paddleocr import PaddleOCR from ultralytics import YOLO class PlateRecognizer: def __init__(self, model_pathbest.pt): self.detector YOLO(model_path) self.ocr PaddleOCR(use_textline_orientationFalse, langch) def recognize(self, img_path): # 第一步YOLOv11检测车牌位置 results self.detector.predict(img_path, conf0.35) plates [] for result in results: boxes result.boxes.xyxy.cpu().numpy() for box in boxes: x1, y1, x2, y2 map(int, box) img cv2.imread(img_path) crop img[y1:y2, x1:x2] # 放大车牌区域有助于OCR识别 crop cv2.resize(crop, (0, 0), fx2, fy2, interpolationcv2.INTER_CUBIC) ocr_result self.ocr.predict(crop) # 提取识别文本 for res in ocr_result: text res[rec_texts][0] if res.get(rec_texts) else plates.append(text) return plates这里有一个很重要的预处理细节车牌区域裁剪后务必放大2-3倍再送OCR。因为YOLOv11输出的车牌框框住的区域字符高度可能只有20-40像素PaddleOCR对这种小字体的识别准确率会下降放大以后字符宽度变大识别率显著提升。实测从放大前的92%左右提升到97%以上这个操作对准确率的贡献比换模型还要明显。5.2 乱码问题来源与排查链路“paddleocr文字识别乱码”是高频搜索词我这里把所有乱码的可能原因和排查方式列全。**第一种图片本身没问题但识别出来一堆不认识的字符。**这个属于模型识别错误比如蓝色车牌反光导致字符不清晰或者车牌倾斜角度过大。解决方法是在送OCR之前先做图像预处理转灰度、高斯模糊降噪、自适应阈值二值化。但注意不要过度二值化PaddleOCR训练时用的是自然图像二值化图反而可能脱离数据分布导致识别率下降。我的经验是只做灰度化和直方图均衡化效果就已足够。**第二种中文车牌汉字被识别成乱码。**比如“粤B12345”被识别成“B12345”或者乱码汉字。这个大概率是模型语言包问题PaddleOCR的langch参数必须带上否则默认按英文识别中文汉字自然全乱。另外use_textline_orientationTrue参数在某些PaddleOCR版本会把竖排文本当作旋转文本处理横排车牌反而可能被误旋转建议设成False。**第三种输出到终端或文件时乱码。**这个不是OCR的问题而是Windows终端编码问题。Python在Windows下输出中文字符到CMD窗口默认用GBK编码如果你设置了PYTHONIOENCODINGutf-8或者在代码里sys.stdout.reconfigure(encodingutf-8)终端就能正常显示。如果你把识别结果写到文件记得写文件时指定编码with open(result.txt, w, encodingutf-8) as f: f.write(text)**第四种保存结果图片时中文乱码。**用OpenCV的cv2.imwrite()保存文件名含中文的图片在Windows下会报错或乱码。解决方法是先用cv2.imencode转成缓冲区再写文件ret, buf cv2.imencode(.jpg, crop) with open(f{plate_text}.jpg, wb) as f: f.write(buf.tobytes())这四种乱码的排查顺序我建议是先检查终端和文件编码再检查语言包设置最后才怀疑模型识别能力。因为前两个是确定性bug好解决模型识别错误则需要反复调图像预处理参数。5.3 PyInstaller打包PaddleOCR如果你做的是一个要发给别人用的工具大概率会用到“pyinstaller打包paddleocr”这个操作。PaddleOCR 3.x打包时有个巨坑模型文件不在包内运行时自动从网上下载。直接打包出来的exe在没联网或者模型缓存目录被清理的机器上会报错找不到模型文件。解决办法是提前把模型下载好然后在代码里指定模型路径from paddleocr import PaddleOCR ocr PaddleOCR( use_textline_orientationFalse, langch, det_model_dir./models/ch_PP-OCRv4_det_infer/, rec_model_dir./models/ch_PP-OCRv4_rec_infer/ )模型文件在PaddleOCR首次运行时输出日志里会给出缓存路径一般在用户主目录下的.paddlex/official_models目录把里面的模型复制到项目目录下再打包时用--add-data把模型文件一并打进去pyinstaller -F main.py --add-data models;models注意PaddleOCR 3.x还有一些动态库依赖打包时可能缺paddle的DLL解决办法是打包前先pip install paddlepaddle确保主环境正常如果还缺就用--hidden-import逐个补上缺失的模块。说实话PyInstaller打包PaddleOCR这个大活没有一次成功的我那次搞了快一天最终靠-F加--collect-all paddleocr和--collect-all paddle勉强打出了可用的单文件exe文件体积膨胀到600多MB功能倒是能跑。所以如果你的使用场景允许装Python环境别急着打包exe这个成本真的不低。6. OpenMV与STM32串口通信数据链路设计得稳系统才不抽风视觉识别做完结果要送到STM32执行通信链路设计决定了整个系统稳不稳。OpenMV和STM32之间用UART串口通信这个选择有几个现实原因实现简单、实时性可预测、MicroPython的UART接口非常易用。USB通信虽然也支持但涉及更复杂的OTG和端点管理在STM32裸机环境下徒增复杂度不是刚需就不碰。6.1 通信协议设计串口通信最忌讳的就是裸传字符串。一旦数据里混入换行符或者干扰字节接收端解析必然错位。推荐的自定义协议帧格式如下帧头数据长度命令字数据区校验和2字节 0xAA 0x551字节 N1字节N字节1字节 CRC8约定如下帧头固定为0xAA 0x55用来做帧同步。数据长度字段表示命令字之后、校验和之前的数据总长度。命令字区分不同业务比如0x01表示车牌识别结果0x02表示车辆触发信号0x03表示测速数据。校验和用累加和从数据长度到数据区所有字节相加取低8位。为什么不用复杂的CRC16因为串口误码率本身很低累加和校验足以应付这个场景而且OpenMV端用MicroPython实现CRC16反而会消耗不少CPU时间。如果链路更长、干扰更强再升级到CRC16也不迟。6.2 OpenMV端程序设计OpenMV端的职责是接收PC发来的车牌识别结果或者自己完成粗定位后把数据转发给STM32。这里给出一个完整示例OpenMV通过UART3连接STM32的USART1import sensor import image import time import ustruct from pyb import UART # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time2000) # 初始化串口 uart UART(3, 115200, timeout_char1000) def send_frame(cmd, payload: bytes): data_len len(payload) frame bytearray() frame b\xAA\x55 frame bytes([data_len, cmd]) frame payload checksum (sum(frame[2:])) 0xFF frame bytes([checksum]) uart.write(frame) def parse_plate_info(buf): 解析PC端发来的车牌文本 格式假设为: GB/T14 简单的JSON或键值对 # 实际这里根据你的传输格式解析 text buf.decode(utf-8, errorsignore) return text # 主循环 while True: # 1. 简单帧同步接收 if uart.any() 5: header uart.read(2) if header b\xAA\x55: data_len uart.read(1)[0] cmd uart.read(1)[0] payload uart.read(data_len) checksum uart.read(1)[0] calc (sum([data_len, cmd]) sum(payload)) 0xFF if calc checksum: if cmd 0x01: plate_text parse_plate_info(payload) # 这里可以根据需要做二次确认或直接转发给STM32 send_frame(0x01, plate_text.encode(utf-8)) # 2. 也可以定期触发拍照粗定位确认车辆是否进入 img sensor.snapshot() # 简单颜色阈值判断蓝底车牌 blue_ranges [(30, 80, 30, 80, -10, 30)] blobs img.find_blobs(blue_ranges, pixels_threshold100, area_threshold100) for blob in blobs: if blob.area() 800: # 发现疑似车牌区域发送触发信号给STM32准备执行 send_frame(0x02, b\x01) time.sleep(1)注意两个细节波特率选115200OpenMV和STM32两边都设置为这个值。波特率再高容易在长线传输中出现误码再低则数据吞吐量跟不上115200是稳定性和速度的平衡点。帧同步逻辑必须用状态机。上面示例是简化版如果数据长度字段N读取失败或者校验和错误接收端要能自动丢弃当前帧重新等待帧头。STM32端我建议用经典的HAL_UART_Receive_IT接收单字节中断在中断里跑状态机每收一个字节判断当前状态逐字节推进。这种方式的优势是内存占用小、逻辑清晰而且不会因为一次错误帧就卡死整条链路。6.3 STM32端数据解析与控制STM32端我用标准库HAL库混合写核心代码如下// 定义帧结构体 typedef struct { uint8_t buffer[64]; uint8_t len; uint8_t cmd; uint8_t state; } FrameParser; FrameParser parser {0}; // 状态机状态 #define STATE_HEADER1 0 #define STATE_HEADER2 1 #define STATE_DATA_LEN 2 #define STATE_CMD 3 #define STATE_DATA 4 #define STATE_CHECKSUM 5 void UART_StateMachine(uint8_t byte) { switch (parser.state) { case STATE_HEADER1: if (byte 0xAA) parser.state STATE_HEADER2; break; case STATE_HEADER2: if (byte 0x55) parser.state STATE_DATA_LEN; else parser.state STATE_HEADER1; break; case STATE_DATA_LEN: parser.len byte; parser.state STATE_CMD; break; case STATE_CMD: parser.cmd byte; parser.idx 0; parser.state STATE_DATA; break; case STATE_DATA: parser.buffer[parser.idx] byte; if (parser.idx parser.len) { parser.state STATE_CHECKSUM; } break; case STATE_CHECKSUM: uint8_t sum parser.len parser.cmd; for (int i 0; i parser.len; i) { sum parser.buffer[i]; } if (sum byte) { // 帧校验通过执行命令 ExecuteCommand(parser.cmd, parser.buffer, parser.len); } parser.state STATE_HEADER1; break; default: parser.state STATE_HEADER1; break; } } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { uint8_t byte; HAL_UART_Receive(huart, byte, 1, 0); UART_StateMachine(byte); HAL_UART_Receive_IT(huart, byte, 1); } }注意HAL库有个坑HAL_UART_Receive_IT每次只能接收指定长度的数据收完一个字节后中断就关闭了必须在回调里重新开启一次接收。很多人写了第一次中断后系统就再没数据进来就是忘了这句重新使能。ExecuteCommand函数里根据命令字执行不同动作void ExecuteCommand(uint8_t cmd, uint8_t* data, uint8_t len) { switch (cmd) { case 0x01: { // 车牌识别结果存到全局变量或OLED显示 memcpy(plate_text_buf, data, len); plate_text_len len; OLED_ShowString(0, 0, (char*)plate_text_buf); // 如果识别到有效车牌抬起道闸 if (len 5) { Servo_SetAngle(90); // 道闸开启 HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_SET); } break; } case 0x02: { // 车辆触发信号测速或亮灯 if (data[0] 0x01) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } break; } case 0x03: { // 测速数据 uint16_t speed (data[0] 8) | data[1]; OLED_ShowNum(0, 2, speed, 3); break; } } }6.4 串口通信实测踩坑这部分都是真金白银换来的经验。**坑一共地问题导致数据全乱。**第一次联调时OpenMV发出来的数据STM32完全收不到示波器看波形是有的但STM32里就是解析不出来。后来排查发现OpenMV的GND和STM32的GND没接在一起两者电平参考点不一样TTL信号自然无法识别。接上共地线后瞬间正常。这个问题几乎每个玩串口通信的新手都会遇到。**坑二波特率误差导致偶发乱码。**使用外部8MHz晶振的STM32F103C8系统时钟如果配置成72MHz且USART1挂在APB2总线72MHz波特率误差在可接受范围内。但如果用了内部RC震荡误差可能到1%以上长帧数据就会出现偶发错误字节。解决方法是不要用HSI务必配置外部晶振或者降低波特率到9600。**坑三OpenMV和STM32上电时序不一致。**如果OpenMV先上电并立即发送数据而STM32还在复位状态那一帧数据就丢了。解决方法是STM32端接收逻辑不因复位丢数据——把接收状态机设计成始终运行不依赖标志位这样任何时刻进来的字节都能被处理。7. 整机联调从“单模块能跑”到“系统稳定工作”所有模块单独都能跑不等于整合起来能跑。我讲讲我的联调顺序和常见问题排查表照着这个顺序操作能省掉大量瞎折腾时间。7.1 联调步骤第一步先测通STM32和OpenMV的串口链路。不接任何视觉逻辑OpenMV上电后每1秒发一个0xAA 0x55 0x00 0x10 0x10空数据帧命令字0x10STM32收到后翻转一个LED灯。LED稳定闪烁说明物理链路没问题。第二步测通PC到OpenMV的链路。PC上用Python写个简单的串口发送程序把模拟的车牌识别结果字符串发给OpenMVOpenMV收到后能正确转发给STM32。这一步不要直接用真实识别结果先用固定字符串测试确定协议解析无误。第三步接入YOLOv11PaddleOCR真实识别结果。先对单张测试图跑完整流程确认识别结果能正确传到STM32并显示。第四步模拟真实场景连续测试。连续拍摄50辆车统计识别成功率、串口丢帧率。丢帧率超过1%就要检查协议设计或者接线质量。7.2 常见问题排查表现象可能原因排查思路STM32无法识别USB设备USB驱动未安装、用的是劣质ST-Link/V2、供电不足安装STSW-LINK009驱动换数据线最小系统板用外接5V供电不要指望ST-Link的3.3V带负载ST-Link Utility连不上芯片ST-Link固件版本过旧、接线错误、芯片锁死升级ST-Link固件检查SWDIO/SWCLK/RST/GND四根线用-l参数全擦除恢复OpenMV程序能跑但串口无输出UART引脚选择错误、和LCD等外设冲突查看OpenMV引脚复用表UART3的TX/RX是P4/P5确认没被其他外设占用PaddleOCR识别中文车牌乱码langch未设置、图像模糊、终端编码先确认langch再看终端编码最后做图片预处理YOLOv11检测漏检远景车辆小目标问题加SAHI切片推理、提高imgsz、或者把摄像头安装高度调低舵机通电后抖动供电电压不足MG996R堵转电流可达2A必须用独立电源供电千万不能让STM32的3.3V去带舵机这里重点说“stm32无法识别usb设备”。Stlink的USB识别失败90%是驱动问题。新版ST-Link V2的驱动在Win10/11上一般能自动识别如果不行就去ST官网下载STM32 ST-LINK Utility安装包自带驱动装完再插设备基本能解决。还有10%的情况是线材问题——ST-Link V2的接口是MINI USB很多劣质mini数据线只供电不传数据换线就能解决。7.3 系统性能优化思路联调通过以后还可以从几个方向提升系统的实战能力。**优化方向一识别结果的置信度校验与容错。**实际车牌中每个字符都有固定格式省份汉字发牌机关代号序号可以在PC端加一个正则校验import re plate_pattern re.compile(r^[\u4e00-\u9fa5][A-Z][A-Z0-9]{5,6}$) def is_valid_plate(text: str) - bool: return bool(plate_pattern.match(text))如果识别结果不符合车牌格式系统不执行任何动作可以再补拍一帧重新识别而不是把乱码字符串直接发给道闸执行。这一步能拦截掉大量错误识别比盲目调模型参数有效得多。**优化方向二处理夜间和逆光场景。**停车场夜间识别是老大难。实际项目中我做了两个改进一是摄像头开启红外补光或者LED补光把环境光亮度拉到稳定区间二是在PC端做图像增强检测到帧平均亮度低于某个阈值时自动做自适应直方图均衡化再送OCR。这两个改动能把夜间识别率从70%左右拉到85%以上。注意不是所有图像都应该做同样的预处理要根据实际亮度动态选择。**优化方向三增加测速联动。**接回前面说的“STM32测频法”用两个红外对射传感器或者地感线圈车辆经过第一个传感器时开启定时器经过第二个时捕获定时器计数值两个传感器距离除以时间差就得到车速// 假设两个传感器相距3米 // TIM2计数频率1MHz float car_len 3.0; // 米 float speed_ms car_len / ((float)timer_count / 1000000.0); float speed_kmh speed_ms * 3.6;把这个速度和车牌识别结果绑定发送就能做到“抓拍超速车辆并联动道闸”的校园/园区场景应用。实际部署中测速精度的关键在于两个传感器之间的距离精确测量以及信号触发沿的选择建议用下降沿否则误差会很大。8. 最后一点实在话这套系统做完我最大的感受是车牌识别本身已经不是难点YOLOv11和PaddleOCR把模型门槛拉到了几乎为零真正决定项目成败的是系统工程能力——各模块能不能稳定通信、协议是否健壮、供电有没有余量、现场环境适不适合摄像头发挥。个人建议三个发力点一是先把串口链路做到不丢帧不乱码再追求识别率链路不稳一切都白搭二是训练数据里一定要混入现场实拍照片纯开源数据集训练的模型在真实角度和光照下会打折扣三是给PC端画一个简单的可视化界面哪怕只是Tkinter窗口实时显示图像和识别结果调试时的效率提升也是肉眼可见的。还有一个小技巧值得分享调试阶段把OpenMV、STM32、PC三者的日志时间戳都打上一旦出现“识别成功但道闸没动”这类问题对比三份日志的时间线就能快速定位是传输丢了、解析错了还是执行器没驱动起来。没有时间戳的日志排查问题就是大海捞针。这套“三级日志联动排查法”在我做过的好几个嵌入式视觉项目里都救了急你也可以试试。
网站建设高端定制企业官网