资讯详情

资讯详情

STM32F407驱动无晶振OV7670实现车牌与颜色识别

大概半年前我从抽屉里翻出一块吃灰很久的OV7670模块型号还偏偏是最“素”的那种无晶振、无FIFO。当时手头正好有STM32F407开发板想着干脆用它做一套车牌识别加颜色识别的小项目。说实话第一次接线时心里是有点打鼓的——无FIFO意味着摄像头的像素时钟直接灌进MCU时序稍有不对就是一帧花屏无晶振意味着这块板子连自己的时钟源都没有得靠主控给它喂频率。但调通之后回头看这套方案恰恰是低成本入门嵌入式视觉最合适的练手路径OV7670便宜、资料多驱动吃透了换成OV7725、OV2640这类无FIFO输出RGB565的摄像头软件基本不需要大改。这个项目能做的事情很明确摄像头采集图像MCU端完成灰度化、二值化、车牌定位、车牌底色识别再配合模板匹配或串口上位机完成字符识别。分辨率降到160x120之后纯MCU可以跑到每秒一到两帧适合做课设、竞赛Demo也适合想完整走一遍“采集-预处理-特征提取-识别”流程的嵌入式爱好者。如果你已经有STM32基础但还没碰过DCMI接口和图像算法那这个项目正好。1. 动手之前先把任务边界和硬件选型想清楚1.1 为什么这个项目选择“无晶振、无FIFO”的OV7670市面上OV7670模块大致分两类。一类是带FIFO常见AL422B的摄像头先把整帧数据写进FIFO缓存MCU什么时候想去读就什么时候去读对实时性要求低很多。另一类就是我用的这种无FIFO板子摄像头的数据线D0-D7、像素时钟PCLK、行同步HREF、场同步VSYNC直接引出来MCU必须实时接住这些数据。无FIFO听起来吓人实际用起来反而干净。STM32F4系列自带DCMI外设这个外设就是专门为并行摄像头设计的配合DMA能把像素流直接搬进内存CPU几乎不用参与。相比FIFO方案少了一颗缓存芯片少了一堆接线成本更低PCB面积也小。坏处是调试时你没有“暂停缓冲”的余地DCMI的极性配置、DMA的长度和速度任何一个环节不对画面立刻给你脸色看。无晶振模块则是把板载晶振省了留一个XCLK引脚让你自己从外部喂时钟。OV7670的XVCLK范围大约在10MHz到48MHz之间常见做法是给24MHz。这里有一点容易忽略不是时钟越高越好也不是越低越好。太高了芯片发热、PCLK也跟着高DMA压力大太低了帧率上不去内部时序还会出怪问题。我之前试过用8MHz喂它图像能出但帧率明显慢而且偶尔会有行错位。老老实实给24MHz最稳。1.2 F4适合做什么不适合做什么STM32F407主频168MHz带单精度FPU192KB SRAM还有DCMI和DMA。这个配置在MCU里算是不错的了但和跑Linux的Cortex-A平台比差距还是代际级的。所以拿到这个项目第一件事不是急着写算法而是想清楚哪些事在F4上能做哪些事硬做会翻车。F4适合做的是图像采集、灰度化、二值化、滤波、边缘投影、基于颜色统计的粗定位、小尺寸模板匹配。这些操作计算量可控160x120分辨率的灰度图也就不到20KB几个循环下来十几毫秒搞定。F4不适合做的是大分辨率下的深度学习、大规模字符库匹配、复杂场景下的鲁棒识别。你把一个Canny边缘检测跑在320x240画面上帧率会掉到让人绝望的程度。我踩过最深的坑之一就是一开始想在F407上完整跑OpenCV式的车牌识别流程折腾几天后顿悟MCU端的核心价值是“快、稳、省电”识别这类重活应该拆出去。所以这个项目的正确打开方式是把F4当成“带算法的采集前端”而不是“小号PC”。1.3 一上来就分好工哪些算法留在MCU哪些交给上位机我最后采用的分工方案是这样的任务放在哪端原因图像采集MCUDCMIDMA没有比硬件外设更合适的了灰度化、滤波、二值化MCU简单重复运算F4完全能扛车牌候选区定位MCU用颜色统计和投影法计算量小车牌底色识别MCU只在ROI里做HSV统计非常快字符分割MCU垂直投影法简单直接字符识别MCU或PC小模板库可在MCU跑复杂字符库建议PC最终决策与展示MCU控制继电器、屏幕显示、语音播报等如果你只是交一个课程设计字符识别也可以全部留在MCU上。限定了省份汉字模板库之后后六位字母数字识别用模板匹配是可行的。但如果你想做真实场景的车牌识别比如停车场那种任意角度、任意光照还是老老实实把裁剪后的ROI通过串口发给PC让OpenCV去处理收尾。这点心态摆正了后面开发会顺很多。2. OV7670无晶振驱动时钟、SCCB与DCMI三件套2.1 第一步从MCU输出24MHz时钟喂给摄像头无晶振模块到手第一件事就是给它造一个时钟。我用的方法是拿定时器输出PWM频率24MHz占空比50%接到摄像头的XCLK引脚。F407的定时器时钟可以有168MHz要产生24MHz分频系数就是7。下面是TIM1输出PWM的初始化片段/* 定时器时钟假设为 168 MHz */ htim1.Instance TIM1; htim1.Init.Prescaler 0; htim1.Init.Period 6; /* 168MHz / (6 1) 24MHz */ htim1.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(htim1); TIM_OC_InitTypeDef oc {0}; oc.OCMode TIM_OCMODE_PWM1; oc.Pulse 3; /* 50% 占空比 */ oc.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim1, oc, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1);TIM1_CH1的复用引脚一般可以在PA8或PE9上引出具体看你的板子和CubeMX里的引脚分配。注意如果你的定时器时钟不是168MHz也不想整除那稍微偏离24MHz也没关系OV7670对XVCLK不苛刻关键是稳定。千万别在运行途中乱改PWM频率摄像头会直接花屏。给电顺序也有讲究先把PWM跑起来等几毫秒让摄像头时钟稳定再拉低复位脚然后拉高最后才开始SCCB初始化。不少模块上电不工作不是配置错了而是顺序反了。2.2 第二步SCCB初始化读不到ID就先别往下走SCCB本质上是I2C的变种OV7670只有一条SIO_C和一条SIO_D7位器件地址写是0x21左移一位后写地址0x42、读地址0x43。初始化SCCB的第一步永远是读芯片ID也就是寄存器0x0A和0x0B的值正常读回0x76和0x73。能读到ID说明时钟、电源、复位、地址全都是好的再往下配寄存器才有意义。SCCB和I2C有个细节差异读操作要发“重复起始”。完整流程是Start - 写地址0x42 - 寄存器地址 - 重复Start - 读地址0x43 - 读数据 - Stop。很多人直接套用I2C读函数读不出来往往就是少了这个重复起始。uint8_t ov7670_read_reg(uint8_t reg) { uint8_t val 0; sccb_start(); sccb_write_byte(0x42, 1); /* 写地址 */ sccb_write_byte(reg, 1); sccb_start(); /* 重复起始 */ sccb_write_byte(0x43, 1); /* 读地址 */ val sccb_read_byte(0); /* 最后一个字节回NACK */ sccb_stop(); return val; }SCCB速率建议压在100kHz到400kHz之间。你可能会觉得越开越快越好但实际上OV7670内部逻辑并不快时钟太快会出现偶发读写失败表现是画面偶尔偏色、寄存器配置“好像没生效”。我最终用了约200kHz稳得很。寄存器配置这里直接搬网上整表配置其实问题不大只要注意几个关键项COM7设置为0x80选择RGB565输出COM15设置为0xD0配置RGB565全范围输出CLKRC设置内部PLL与分频。分辨率的裁剪方式很多可以直接在窗口寄存器组里调HSTART、HSTOP、VSTART、VSTOP也可以按模块默认的VGA时序采集软件里只取中间一块ROI。这两种我试过后者更省事因为在MCU端做一次坐标变换比折腾寄存器容易得多。2.3 第三步DCMIDMA把像素流接进来DCMI接口的接线非常直接摄像头的D0-D7接DCMI_D0-D7PCLK接DCMI_PIXCLKHREF接DCMI_HSYNCVSYNC接DCMI_VSYNC。具体每个DCMI引脚对应芯片的哪个GPIO不同型号不一样最靠谱的方法是用CubeMX勾选DCMI外设它会把能用的引脚直接标出来。无FIFO方案的硬件连接其实比FIFO方案简单FIFO方案要同时操作读写指针和一堆控制线这里全部交给硬件外设。DCMI配置要点是外部同步模式、8位数据宽度、HSYNC/VSYNC/PCLK的极性。最烦的就是极性不同摄像头模块输出极性可能不一样。我没有示波器调试花屏时直接写了个小函数把四种极性组合轮着试每试一次抓一帧看效果一两分钟就能定位到正确组合。这个办法蠢但有效。DCMI_HandleTypeDef hdcmi {0}; hdcmi.Instance DCMI; hdcmi.Init.SynchroMode DCMI_EXTERNAL_SYNC; hdcmi.Init.PCKPolarity DCMI_PCKPOLARITY_FALLING; hdcmi.Init.VSPolarity DCMI_VSPOLARITY_LOW; hdcmi.Init.HSPolarity DCMI_HSPOLARITY_HIGH; hdcmi.Init.CaptureRate DCMI_CR_ALL_FRAME; hdcmi.Init.ExtendedDataMode DCMI_EXTEND_DATA_8B; HAL_DCMI_Init(hdcmi); HAL_DCMI_Start_DMA(hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)frame_buf, 160 * 120 * 2);DMA要记得用循环模式内存地址自增数据宽度按半字。RGB565一个像素两个字节如果开了DCMI 8位模式DMA这边按半字搬运正好能凑齐像素。采集完成中断里置一个标志位主循环里处理一帧处理完继续让DMA跑下一帧。这样CPU在绝大多数时间里是空闲的。存一帧160x120的RGB565需要37.5KB双缓冲就是75KB。F407的SRAM有192KB完全够用。如果你非要用320x240的QVGA一帧就是150KB双缓冲直接超出内存这时候只能单缓冲加行中断做流水线处理或者干脆用160x120。我的建议是别在F4上追求高分辨率车牌识别用160x120足够处理速度快很多。3. 车牌识别与颜色识别的MCU落地实现3.1 预处理链路从RGB565到二值图OV7670输出的RGB565每个像素16位高5位红色、中间6位绿色、低5位蓝色。灰度化时我直接用整数近似公式uint8_t rgb565_to_gray(uint16_t pixel) { uint8_t r (pixel 11) 0x1F; uint8_t g (pixel 5) 0x3F; uint8_t b pixel 0x1F; /* 扩展到8bit精度 */ r (r 3) | (r 2); g (g 2) | (g 1); b (b 3) | (b 2); return (uint8_t)((77 * r 150 * g 29 * b) 8); }有些项目图省事直接拿G通道当灰度画面会偏暗对比度不够后面车牌定位容易翻车。灰度化之后建议做一次3x3中值滤波去一下传感器噪声。代价是每像素9次比较但160x120也就是不到两万像素F4跑起来也就几毫秒。二值化不要用固定阈值。光照一变固定阈值立刻失效。我试过上午调好的阈值下午太阳一斜就识别不到车牌。后来改成大津法按灰度直方图动态算阈值效果好得多。大津法原理就是遍历0到255的所有阈值选出让前景和背景类间方差最大的那个。计算量其实不大灰度直方图是现成的遍历256次也是一瞬间的事。uint8_t otsu_threshold(uint32_t *hist, uint32_t total) { uint32_t sum 0; for (int i 0; i 256; i) sum i * hist[i]; uint32_t sumB 0, wB 0; double max_sigma -1.0; uint8_t threshold 0; for (int t 0; t 256; t) { wB hist[t]; if (wB 0) continue; uint32_t wF total - wB; if (wF 0) break; sumB t * hist[t]; double mB (double)sumB / wB; double mF (double)(sum - sumB) / wF; double sigma (double)wB * wF * (mB - mF) * (mB - mF); if (sigma max_sigma) { max_sigma sigma; threshold (uint8_t)t; } } return threshold; }3.2 车牌定位颜色粗筛加边缘投影细定位车牌识别的第一步不是直接找字符而是先找到“车牌在哪里”。我用的是两层策略颜色粗筛加边缘投影。车牌底色是强约束。传统蓝牌、黄牌、新能源绿牌、白牌颜色区域非常明显。把图像转到HSV空间对每个像素判断它属于哪一类颜色。具体转换公式网上很多我这里只说判断逻辑车牌类型H范围0到360度辅助判断蓝色200到235S大于0.35黄色35到65S大于0.35绿色新能源90到150S大于0.35白色不依赖HV高且S低颜色粗筛不是直接找全图最大色块那样容易把广告牌、蓝衣服都算进来。我的做法是先做颜色掩码把蓝色或你要识别的那种颜色像素标记为1其余为0然后找面积足够大的连通域。连通域用游程编码加并查集实现代码量不大在160x120上很快。找到候选连通域之后再用边缘投影精确切边。对灰度图做一次简单的Sobel水平差分绝对值累加后做水平投影车牌区域会因为上下边框和字符形成两个明显的峰值柱。再在两条边缘之间做垂直投影就能把左右边界也定下来。这一步做完车牌ROI的四个角基本就准了。确定ROI之后颜色识别就很简单了统计ROI内部HSV颜色直方图取占比最高的色相类别就是车牌底色。比在全图做颜色识别稳定得多因为背景干扰已经被边缘投影过滤掉了。3.3 字符分割与模板匹配车牌区域切出来之后字符分割我用垂直投影。对二值化ROI的每一列统计白色像素数字符间隙位置数值接近0字符列数值明显偏高。连续非零段就是字符候选。这里有一个细节很多人会忽略车牌字符间的空隙不是完全均匀的第二个字符和第三个字符之间间距明显比别处大。如果直接按投影谷底切容易把这里误判成一个字符。我的办法是结合先验正常车牌除去汉字和间隔符后面有6个字符。如果投影切出来少于6段或多于6段就把候选段宽度列出来按中位数宽度做合并或拆分调整。这个修正对识别率提升非常关键。字符识别我用的是模板匹配。把每个字符归一化到20x40的二进制图然后和模板逐像素比较重合度最高的就是识别结果。模板库里只要有字母A-Z、数字0-9就行因为很多项目不要求识别省份汉字只识别后六位。如果有精力加汉字那就按你实际要用的省份去收集模板不要一次性做全套工作量太大了。模板匹配的相似度计算要注意字符内部笔画粗细不同完全二值化后边缘容易抖动。我试过直接按像素异或效果一般。后来改成把字符图缩放到16x32再统计每个4x4小格子里的白色密度生成一个64维的特征向量匹配时比较特征距离。这样对笔画细微抖动不敏感识别率明显上升。3.4 混合方案MCU筛ROI、上位机做最后识别如果你发现MCU端模板匹配识别率不够别死磕。我后来在另一个迭代版本里改成了混合方案MCU仍然负责采集、预处理、定位把最终裁剪出来的车牌ROI二值图通过串口发给电脑电脑用OpenCV做字符识别。这样MCU的实时性优势和PC的算法优势都保住了。串口传输一帧160x120的灰度图去掉协议头大约19KB。921600波特率下理论传输时间约0.17秒实际加协议开销接近0.25秒。如果嫌慢只裁剪车牌ROI发送常见车牌ROI也就80x24左右一帧不到2KB延迟直接降到几十毫秒。这也是为什么我不建议MCU把整帧原始数据往外面传传数据要有目的传ROI比传全图高效得多。# 上位机伪代码接收串口帧后进行最后的字符识别 import cv2 import numpy as np data serial_read_frame() # 自定义串口协议取一帧ROI gray np.frombuffer(data, dtypenp.uint8).reshape(24, 80) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) contours cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] # 对轮廓排序、切字符、做模板匹配或直接走OCR这个方案跑起来非常稳MCU端代码复杂度也降了整机功耗还低。课程设计、电子竞赛如果时间紧我强烈建议直接走这条路把MCU端模板匹配当备选。4. 调试实录无FIFO摄像头最常见的几类故障4.1 从SCCB不通到图像花屏我踩过的坑这个项目我前后折腾了大概两周第一天几乎全部耗在SCCB上。摄像头模块上电后读ID一直返回0xFF排查到最后发现是两个问题叠在一起。第一SCCB的两根线没有接外部上拉虽然芯片内部有弱上拉但信号沿太慢时钟一高就出错。后来补了两个4.7k欧上拉电阻情况立刻好转。第二模块的PWDN引脚悬空了这个引脚需要拉低才能正常工作悬空状态下摄像头处于掉电模式SCCB自然没反应。把PWDN接地之后ID一下就出来了。SCCB通了之后又来一个新问题图像花屏。现象是能看出大概轮廓但有大量横条纹和错位。这是DCMI的极性问题和DMA配置问题混在一起。我先固定DMA长度一帧160x120x2字节然后四组极性挨个测最后发现PCLK要在下降沿采样才稳定。花屏解决之后又有颜色问题画面整体发红车牌上的白色字符看起来带紫边。查了资料发现是RGB565的字节顺序反了DMA收到的两个字节需要交换一下。我在接收后做了一次字节交换颜色立刻正常。再往后是画面撕裂。上半帧是上一场的下半帧是这一场的中间有明显断层。这个就是DMA缓冲区被覆盖的问题。中断里处理图像花了太长时间下一帧已经开始写入同一个缓冲区。解决办法是双缓冲或者把处理逻辑改到主循环里中断里只置标志位。我选了双缓冲加主循环处理撕裂问题彻底消失。4.2 快速排查表现象可能原因排查办法SCCB读ID返回0xFF没给时钟、PWDN拉高、SCCB无上拉、速率过快先确认PWM输出PWDN接地补上拉电阻降低SCCB频率图像全黑PWDN悬空、曝光时间太短、VSYNC极性反了拉低PWDN检查镜头盖尝试翻转VSPOL画面偏红或偏蓝RGB565字节顺序反了、白平衡寄存器没固定交换字节顺序固定AWB寄存器花屏或有横条纹PCLK采样沿不对、DMA传输长度错误试四种极性组合核对DMA数据量画面上下撕裂DMA缓冲区不足、中断处理时间过长换双缓冲中断里只置标志位帧率很低分辨率太高、串口打印拖慢、DMA抢占失败降到160x120关掉调试打印调整中断优先级车牌定位不到光照变化导致阈值失效、ROI范围不当改用大津法动态阈值先锁定车牌颜色再投影字符分割错误字符间距不均、噪声干扰用中位数宽度修正投影结果先滤波再二值化4.3 稳定性的几条建议摄像头模组对电源纹波比想象中敏感。我一开始让摄像头和舵机共用一个电源图像每隔几帧就跳一下后来单独用一路LDO供电马上稳了。数据线可以在靠近MCU端串33欧姆电阻减小信号振铃特别是当杜邦线比较长的时候。OV7670没有镜头锁定结构对焦环容易松动。调好焦距后我用热熔胶点了一下固定再也没为模糊画面烦过。光照方面白平衡寄存器建议在正式识别前固定不要让摄像头在运行中自动调整白平衡否则颜色识别的阈值会漂移。每次换拍摄场景可以先拍一张白纸做一次简单的白平衡校准把RGB三通道增益算出来后续用固定增益跑。这套操作对识别率提升非常明显比调算法参数都管用。还有一个容易踩的坑调试阶段别在图像采集中断里用串口打印。printf一次阻塞几百微秒DMA就少收几百个字节画面立刻花给你看。真要打印调试信息采完一帧、处理完之后在主循环里打印或者干脆只打印统计值不要打印像素数组。5. 工程组织与扩展方向5.1 代码模块划分与主循环状态机项目代码如果全塞在一个main.c里后面改起来会崩溃。我按功能拆成了几个模块sccb.cSCCB通信底层读写寄存器ov7670.c摄像头初始化、ID校验、寄存器表配置dcmi.cDCMI和DMA初始化、帧中断回调image.c灰度化、滤波、二值化、投影plate.c车牌定位、颜色识别、字符分割match.c模板匹配或特征匹配主循环用一个简单的状态机while (1) { if (frame_ready) { frame_ready 0; image_rgb565_to_gray(gray_buf, frame_buf, W, H); image_median_filter(gray_buf, tmp_buf, W, H); uint8_t th otsu_threshold(hist, W * H); image_threshold(gray_buf, binary_buf, W, H, th); if (plate_locate(binary_buf, roi) 0) { plate_color_detect(frame_buf, roi, color); plate_split(binary_buf, roi, chars, char_count); for (int i 0; i char_count; i) { chars[i].result match_char(chars[i].data); } } } }很多人喜欢把所有图像处理揉在一个“大函数”里维护起来极其痛苦。按模块拆开之后每个部分都能单独测试。比如我先单独测试灰度化把灰度图通过串口发给PC存成图片看效果确认没问题再写下一步。这种“每一步都可视化验证”的习惯能帮你把问题隔离在最小范围内。5.2 后续还能怎么扩展这套硬件平台能扩展的方向不少。如果你想继续在MCU上折腾可以加FreeRTOS把采集任务和处理任务分开采集到一帧就往队列里扔处理线程慢慢算。DMA缓冲区用双缓冲配合信号量逻辑会清晰很多。如果想换摄像头OV7725的寄存器表和OV7670很接近初始化部分改改就能用。OV2640也支持RGB565输出但它的寄存器有bank切换配置稍微麻烦一点如果让OV2640输出JPEGF4没有硬件解码不推荐在MCU端做。DCMI采集RGB565这套逻辑对它们完全通用。再往工程化走可以加继电器控制闸机、加语音播报模块、加无线模块把识别结果传到服务器。想挑战更高性能可以把整个识别后端迁到Cortex-A平台或者直接PC上跑OpenCVMCU只做采集和通信。你会发现DCMI这套采集逻辑和算法框架在哪个平台都成立换平台只是把C代码换成Python或C的事情。5.3 最后的个人体会做完这个项目我最深的感受是嵌入式视觉的难点往往不在算法本身而在于把外设调稳、把资源抠出来、把光照变化扛住。SCCB不通、图像花屏、颜色偏紫、DMA覆盖这些问题每个都会让你怀疑人生但解决掉之后你对DCMI、DMA、摄像头时序和图像处理的理解会有一个质的提升。如果让我给新手一个建议那就是“先通后优分段验证”先把摄像头画面稳定显示出来再做灰度化再做定位最后才做识别。每一步都确认无误再往下一步走千万不要上来就想直接端到端跑通全套流程。这套项目做完你再去碰树莓派摄像头或者Linux平台上的视觉项目会明显感觉到底层的这些东西其实都是相通的。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →