资讯动态

STM32F4接OV7670无FIFO摄像头实现车牌识别完整方案

发布时间:2026/10/15 15:25:29 来源:尧图企业网站定制
最近在做低速目标识别相关的东西翻出了之前赛车识别和颜色分类的项目记录。不少人在问 STM32F4 接 OV7670 无晶振、无 FIFO 摄像头模块能不能做车牌识别这里统一梳理一下完整思路和可复现的调试记录。先说结论完全能做。这套方案的核心是STM32F407 OV7670 摄像头无晶振、无 FIFO实现两类任务车牌底色识别和车牌字符/底色综合识别。相比带 FIFO 的摄像头模块无晶振、无 FIFO 版本接线少、成本低、实时性更好驱动程序写起来反而更直白。而且 OV7725、OV2640 这类同样走 SCCB DCMI 接口的摄像头移植思路几乎一模一样改改寄存器配置就能跑起来。这篇文章会把硬件接线、底层驱动、颜色识别、车牌定位识别、常见坑一次性说透适合正在做电赛、嵌入式课程设计或者想入门图像采集的同学参考。1. 方案选型与整体设计思路1.1 为什么偏偏选无晶振、无 FIFO 的 OV7670市面上的 OV7670 模块主要有三种形态带晶振带 FIFO、带晶振无 FIFO、无晶振无 FIFO。很多人一上来就买带 FIFO 的觉得省心但实际上 FIFO 版本的问题不少。带 FIFO 的模块常见的是 AL422B 芯片确实能从物理上解决“摄像头数据太快、单片机来不及读”的问题。但代价是你要多学一套 FIFO 的读时序还要等摄像头把一整帧写进 FIFO 才能读帧率直接砍半。而 STM32F4 本身就有 DCMI 摄像头数字接口可以直接接收并行像素数据压根不需要中间缓冲。所以从外设资源角度看无 FIFO 版本才是和 STM32F4 最搭的。无晶振版本则意味着摄像头模块上没有独立的时钟晶振XCLK 时钟由 STM32 的 MCO 引脚直接供给。这样做的好处有三个频率可以自己调、模块体积更小、成本更低。对做车牌识别这种“颜色和轮廓主导”的场景来说XCLK 稍微给低一点完全不影响效果反而还能降低 PCB 布线难度。门槛也有。无 FIFO 意味着数据流是连续不断的你必须用 DMA 接力并且要把帧同步、行同步、像素时钟的时序理清楚。这套逻辑一旦跑通后面接 OV7725、OV2640 都是同样套路。1.2 STM32F4 做图像处理的主控优势有人可能会问为什么不用树莓派或者直接用 OpenMV原因很现实很多竞赛平台限定了主控而且 STM32F4 本身做轻量图像处理并不算吃力。STM32F407 主频 168MHz片上带有DCMI 接口支持 8/10/12/14 位并行数据内置 DMA 请求。这意味着摄像头数据可以绕过 CPU直接以“满带宽”灌进内存CPU 可以在 DMA 传输期间去跑其他逻辑。内存方面F407 有 192KB SRAM。如果开 QVGA 分辨率320x240的 RGB565一帧数据是 320x240x2 153600 字节大约 150KB单片缓冲放得下。做颜色识别和车牌定位绰绰有余。想要流畅的双缓冲需要外扩 SRAM 或者切到 QQVGA这个后面细说。另外STM32F4 的 CPU 跑整数运算很快像 RGB 转 HSV、颜色阈值分割、模板匹配这类操作用整数近似和查表优化完全可以在帧间隔内处理完。1.3 OV7725、OV2640 的兼容思路OV7725 同样出自原豪威OmniVision传感器约 30 万像素接口和 OV7670 几乎一致SCCB 控制寄存器的地址和含义大体相同但分辨率、默认输出格式、AGC/AEC 参数有些区别。移植时核心工作是重写寄存器初始化数组DCMI 和 DMA 采集代码可以直接复用。OV2640 更特殊一些它最高支持 200 万像素但寄存器配置复杂不少需要通过 JPEG 模式或 RGB565 模式输出。做车牌识别时建议把分辨率降到 SVGA800x600以下DCMI 数据宽度配置为 8 位同样可以复用这套方案。其中要留意的是各家传感器对HREF/VSYNC/PCLK 的极性定义略有差异。DCMI 初始化中的极性配置必须参考具体传感器数据手册这也是大多数兼容性 bug 的根源。2. 硬件连接与底层驱动要点2.1 引脚分配与硬件接线先给出一组我实际用过的接线以 STM32F407ZGT6 为例摄像头直接接 DCMI 引脚。D0 到 D7 是 8 位并行数据总线PCLK 是像素时钟VSYNC 是帧同步HREF 是行同步。OV7670 引脚STM32F407 引脚说明D0 ~ D3PA0 ~ PA3DCMI_D0 ~ DCMI_D3D4 ~ D7PB0 ~ PB3DCMI_D4 ~ DCMI_D7PCLKPA6DCMI_PIXCKVSYNCPA4DCMI_VSYNCHREFPA7DCMI_HSYNCXCLKPA8MCO1 输出时钟SIO_C / SIO_DPB6 / PB7软件模拟 SCCBPWDN直接拉低或接 GPIO 低电平上电后禁用掉电模式RESET接 RC 复位或 GPIO初始化时拉低再拉高注意PB2 是 BOOT1 引脚我一般避免在 PB2 上接数据线所以 D0~D3 用 PA0~PA3D4~D7 用 PB0~PB3 这种组合可以很好地避开 BOOT 相关引脚冲突。如果开发板引出的 DCMI 数据引脚固定了也可以查参考手册的复用表确认可用引脚。SCCB 我建议直接用 GPIO 模拟不要用硬件 I2C 外设。OV7670 的 SCCB 协议和标准 I2C 大体兼容但有细节差异GPIO 模拟可以完全掌控时序排查问题也方便。2.2 SCCB 总线配置与寄存器读写SCCB 有三条核心规则起始和停止条件与 I2C 类似设备地址通常是 0x42写地址寄存器地址 8 位数据 8 位。无晶振模块从机时钟不受 XCLK 限制但 SIO_C 的时钟频率不要超过 400kHz。软件模拟的时序非常简单起始SIO_D 拉低SIO_C 保持高。发设备地址 0x42。发寄存器地址。发数据字节。停止。需要注意OV7670 的 SCCB 读操作要先写寄存器地址然后重新给起始条件再发读地址 0x43才能读出数据。很多第一次写的同学直接在写地址后立刻读结果读回来全是 0xFF。调试 SIO 通信时强烈建议把读写函数封装好之后先回读摄像头 ID 寄存器PID 寄存器地址 0x0A应该读回 0x76验证通路没问题再往下走。2.3 关键寄存器配置与参数计算OV7670 寄存器很多但不是每个都要改。做 RGB565 QVGA 输出时这几个寄存器必须重点对待寄存器地址名称作用0x11CLKRC内部时钟分频影响 PCLK0x12COM7分辨率、输出格式、RGB 使能0x40COM15RGB 输出格式RGB565/555/4440x3ATSLB字节顺序、U/V 顺序错了会红蓝互换0x3DCOM12颜色饱和度、延迟等0x13COM8AGC / AEC 开关自动曝光总控0x14COM9AGC 增益上限0x17HSTART行起始位置配合窗口水平偏移0x18HSTOP行结束位置0x19VSTART帧起始位置0x1AVSTOP帧结束位置0x03COM3水平/垂直镜像等关于 XCLK 的频率我实测用 8MHz 完全足够。168MHz 主频下MCO1 可以配置为 PLL 输出 8MHz代码上就是HAL_RCC_MCOConfig选RCC_MCO1SOURCE_PLLCLK再配分频系数。如果 XCLK 太高PCLK 也会跟着变快DMA 容易丢像素太低则帧率上不去。QVGA 下 8MHz 输入内部再分频PCLK 大约能做到 6MHz帧率大概 10~15 帧做车牌识别足够。分辨率设置要同步改HSTART/HSTOP 和 VSTART/VSTOP。网上很多初始化代码默认是 VGA640x480如果 DMA 缓冲区按 QVGA 的宽度配图像会斜切。我习惯直接参考摄像头数据手册的“Recommended Settings”把 QVGA 的窗口寄存器整套搬过来再统一修改。3. 图像采集与前置处理3.1 无 FIFO 模式下的 DCMI DMA 采集无 FIFO 模式下摄像头输出的是连续像素流DCMI 外设负责抓取这些像素DMA 负责把像素搬到内存。配置思路是这样的DCMI 工作模式外部同步模式即由 VSYNC/HREF/PCLK 控制同步。数据宽度8位。极性配置VSYNC 高有效、HREF 高有效、PCLK 上升沿采样。DMA使能循环模式每次传输长度为一帧字节数例如 QVGA RGB565 就是 320x240x2约 153600 个半字。注意外设地址是 DCMI 的数据寄存器内存地址是帧缓冲区PSIZE 和 MSIZE 都配成半字HalfWord。帧完成中断里要做的是“关闭这次 DMA 传输处理这一帧数据再把 DMA 重新拉起来”。如果不想丢帧可以在帧中断里只交换缓冲区指针让 DMA 继续跑。HAL 库的调用大致是这样DCMI_HandleTypeDef hdcmi; hdcmi.Instance DCMI; hdcmi.Init.SynchroMode DCMI_SYNCHRO_HALF; hdcmi.Init.PCKPolarity DCMI_PCKPOLARITY_RISING; hdcmi.Init.VSPolarity DCMI_VSPOLARITY_HIGH; hdcmi.Init.HSPolarity DCMI_HSPOLARITY_HIGH; HAL_DCMI_Init(hdcmi); HAL_DCMI_Start_DMA(hdcmi, DCMI_MODE_CONTINUOUS, (uint32_t)frame_buffer, IMG_WIDTH * IMG_HEIGHT * 2);有一点必须注意DMA 配置的传输单元是字节还是半字直接决定图像是不是错位的。OV7670 在 RGB565 模式下每个像素由两个字节组成所以 DMA 内存宽度和外设宽度都建议配成半字。如果配成单字节帧内数据依然是连续的只是你后续取像素时要注意字节序但配成半字能天然保证像素对齐。3.2 帧数据在内存中的存储与像素读取RGB565 的每个像素占 2 个字节内存里高位字节在前大端字节序与 TSLB 寄存器设置有关。我习惯把缓冲区定义成uint16_t数组这样取像素直接frame[y * IMG_WIDTH x]不用每次拼字节。拆出 RGB 分量的位运算是这样的uint16_t pixel frame[y * IMG_WIDTH x]; uint8_t r (pixel 11) 0x1F; uint8_t g (pixel 5) 0x3F; uint8_t b pixel 0x1F;然后可以左移分别映射到 8 位空间方便后续统一处理。实际做颜色识别时我一般直接在这 5/6/5 位深度上做阈值最多加一次移位省掉浮点运算。如果发现画面整体偏红偏蓝大概率是 TSLB 寄存器里的字节顺序配置反了。在 OV7670 中可以通过 0x3A 的 bit0 来交换像素字节顺序。这个问题在 DCMI 直采时尤其常见FIFO 版本因为先写入再读出反而会在某一步被掩盖掉最终图像也可能错。3.3 颜色识别算法从 RGB 到 HSV 的整数化颜色识别这件事在 RGB 空间直接做阈值非常痛苦。光照一变R/G/B 三个分量跟着变阈值怎么调都不理想。车牌识别里最常用的是HSV 颜色空间把色相H、饱和度S、亮度V分开这样颜色判断和光照基本无关只依赖色相。RGB 转 HSV 的标准公式是max MAX(r, g, b) min MIN(r, g, b) delta max - min H 如果 delta 0: 0 如果 max r: (60 * ((g - b) / delta) 360) % 360 如果 max g: 60 * ((b - r) / delta) 120 如果 max b: 60 * ((r - g) / delta) 240 S delta / max max ! 0 时 V max嵌入式里我不用浮点全部用定点近似。比如把饱和度映射到 0~255那么s (uint8_t)(delta * 255 / max)。色相用查表或者直接几个分支判断。因为车牌底色就那几种蓝、黄、绿、白不需要高精度色相用分段判断完全够。颜色阈值我建议自己采集样本再统计而不是抄网上的固定值。把摄像头固定好对着一张蓝色车牌纸板拍 100 帧打印每个像素的 HSV 值看 H、S、V 的分布范围。例如我实际调出来的蓝色阈值大概是H: 200~250S: 70~255V: 60~230黄色阈值H: 15~50S: 80~255V: 90~255绿色新能源牌H: 80~160S: 60~255V: 60~230注意 V 通道不能太低太低了基本就是黑色也不能太高太高就是高光/白色很容易误判。遍历像素做颜色判断时把阈值写死成整数宏每个像素只做几次比较速度非常快。4. 车牌识别核心实现4.1 车牌区域定位先找底色再找字符车牌识别第一步不是找字符而是找底色的“色块”。中国车牌是按底色区分类别的蓝底白字、黄底黑字、绿底黑字、白底黑字等底色在整幅画面里是一块颜色均匀的矩形区域。我用的是“行扫描 连续色段统计”的方法比 OpenCV 里那种连通域分析在单片机上实现起来简单得多对每一行从左到右扫描像素。如果像素满足当前要检测的颜色阈值比如蓝色就统计连续存在多少个像素。如果连续蓝色像素数量超过一个阈值比如 50 个就记录该行的起始列和终止列并把这个行段并入候选区域。连续多行都有符合要求的色段说明这是一个高度方向上有一定厚度的色块记录上下边界。得到候选区域后计算宽高比。标准车牌比例大约是 440x140长宽比约 3.1但摄像头斜视、透视变形会改变这个值。我把候选区域的长宽比范围放宽到 1.8~4.5宁可多留候选等后面字符识别再过滤。如果一帧里同时出现多个候选色块还可以用“面积最大”和“位置靠近画面中央”两个规则进一步筛选通常能去掉大部分干扰。4.2 车牌底色与类型判定类型判定其实就是对候选区域内的像素做一次颜色统计。在 HSV 空间里对候选区域内所有像素按蓝、黄、绿、白四类做投票得票最多的就是车牌底色。统计可以用一个简单的循环uint32_t cnt_blue 0, cnt_yellow 0, cnt_green 0, cnt_white 0; for (row top; row bottom; row) { for (col left; col right; col) { pixel frame[row * IMG_WIDTH col]; if (is_blue(pixel)) cnt_blue; else if (is_yellow(pixel)) cnt_yellow; else if (is_green(pixel)) cnt_green; else if (is_white(pixel)) cnt_white; } }统计完之后取最大值同时计算它占整个区域的比例。如果最高占比连 20% 都不到说明这个区域大概率不是车牌直接丢弃。这一步能在字符识别之前就把很多误检排除掉。实际测试中蓝色和绿色有时候会互相混淆因为感光元件对蓝绿通道的响应接近。解决办法是把饱和度阈值提高减少暗光下蓝色计数的误增。4.3 字符分割与识别确定底色后工作区域缩小到候选区内接下来就是字符分割和识别。这里只做二值化。蓝底车牌上字符是白色黄底/绿底上是黑色因此二值化的规则不同。比如蓝底车牌直接取“亮度高于阈值”的像素为字符黄底/绿底车牌取“亮度低于阈值”的像素为字符。我这个项目里没有做传统车牌识别里复杂的倾斜校正一般摄像头尽量摆正透视只要不太夸张字符分割和模板匹配都能扛住。字符分割用垂直投影法。先把候选区每一列的“字符像素”数量统计出来连续非零区域就是一个字符连续零区域就是字符间隔。中国车牌一个典型难点是第一个汉字笔画多、容易和边框粘连处理时我会把上下边缘再各裁掉 3~5 个像素去掉边框干扰。分割出的字符图像归一化到固定尺寸例如高 24、宽 12然后和模板做逐像素异或累加。模板库包含 31 个省份汉字、24 个大写字母通常去掉 I 和 O 防止混淆、10 个数字一共 65 个模板。对每个模板uint16_t diff 0; for (uint16_t i 0; i TEMPLATE_SIZE; i) { diff (sample_bits[i] ! template_bits[i]) ? 1 : 0; }diff 最小且低于阈值的模板就是识别结果。STM32F4 跑一轮 65 个模板、每个 24x12 像素的匹配大约只需要几毫秒完全能接受。有一点实操建议分割比识别重要得多。字符一旦分割歪了、粘连了再好的匹配算法也会翻车。我先保证二值化和投影分割的质量识别本身用最简单的方法就能跑出很高正确率。5. 常见问题与调试心得5.1 图像花屏、雪花、偏色怎么查花屏是最常见的问题优先按这个顺序排查DMA 传输长度和分辨率不匹配。检查width * height * 2是否和 DCMI 实际输出分辨率一致。分辨率改过DMA 长度没改画面必定斜切或花。DCMI 极性配置反了。PCLK 上升沿/下降沿采样的极性反了图像会非常诡异甚至雪花。用示波器看 PCLK 和 D0 的关系或者切换DCMI_PCKPOLARITY_RISING/DCMI_PCKPOLARITY_FALLING直接试。VSYNC/HREF 时序不对。如果图像整体偏移、有大量黑色横条多半是行同步极性或帧同步极性配置问题。摄像头初始化没生效。SCCB 通信失败或者寄存器没写进去输出可能是 RAW 格式或者默认 VGA 格式。先读 PID 验明身份再写关键寄存器。电源纹波太大。摄像头模块的模拟电源对纹波很敏感如果画面有规律横纹检查供电是否干净最好单独用 LDO 给摄像头供电。偏色问题集中在白平衡和字节序上。OV7670 默认自动白平衡在刚上电时可能需要几百毫秒收敛初始化完成后不要立刻抓帧等几帧再开始识别。如果是明显的红蓝互换查 TSLB 寄存器的字节顺序。5.2 帧率与实时性优化车牌识别对帧率要求其实不高但图像处理过程不能卡到响应迟钝。我常用的优化手段分辨率砍到 QVGA 甚至 QQVGA。320x240 只做颜色扫描很快如果跑完整 HSV 全图遍历降到 160x120 能让处理时间减半识别率并不会明显下降。不用浮点。HSV 全部用整数宏判断一个像素只用几十个周期。只处理候选区域。先做一次快扫描定位色块字符识别只在候选区域里做避免全图反复遍历。DMA 双缓冲。如果内存够或者外扩 SRAM双缓冲可以让摄像头采集和 CPU 处理并行帧率几乎翻倍。没有双缓冲时处理期间 DMA 要停掉帧率会掉到 10fps 以下但车牌静态场景一般够用。5.3 官方文档里不会写的几个坑有些问题不是 bug是硬件和传感器本身的脾气这里列出来分享一下PWDN 和 RESET 时序很关键。上电后 PWDN 必须拉低否则摄像头进入掉电模式SCCB 写什么都白搭。RESET 引脚要在初始化前拉低再拉高至少保持几十微秒然后再开始写寄存器。很多“摄像头黑屏”其实是 RESET 没彻底复位。无晶振模块的 XCLK 不能悬空。有的同学从带晶振模块换到无晶振版忘了给 XCLK 接 MCO 输出摄像头输出全是乱码。没有时钟传感器完全无法工作。OV7670 的自动曝光和自动增益对颜色判断影响很大。车牌反光严重时整个画面可能过曝蓝色底变成接近白色。如果场景光照可控建议把 AGC/AEC 固定下来用固定曝光值比自动曝光更稳定。如果光照变化大则保留自动模式但要给 HSV 阈值留出足够的容差。低光照下的蓝色识别最容易翻车。我的经验是把蓝色饱和度阈值放宽到 60 左右同时用亮度下限 60 兜底。宁可误检几个非车牌区域也不能漏掉真实车牌因为后续还有长宽比和字符识别二次过滤。最后分享一个个人习惯每次改完寄存器配置都把“当前配置 现场照片效果”记到笔记本上特别是改镜像、改分辨率、改字节序这类易混项。摄像头调试最怕“每次都在试”有记录才能快速收敛到可用状态。这个项目做完之后OV7725 的移植我基本只花了半天就是因为驱动分层把“采集逻辑”和“传感器配置”完全分开了换摄像头只改寄存器表DCMI 和颜色识别代码一行没动。这也是做嵌入式视觉项目值得坚持的方向。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑