
做FPGA图像处理的开发者十有八九都想过这么一件事能不能用一块FPGA把摄像头采集回来的画面直接在上面完成车牌识别再把结果叠加显示到LCD上全程不走CPU这个想法听起来很过瘾但真做起来里面涉及的东西比想象中多得多。我花了两周多时间把这套从OV5640采集到LCD显示的完整硬件流水线跑通借这篇文章把整个思路和踩过的坑一次说透给想入坑FPGA图像处理的同学留一份参考。这个项目本质上是一个典型的FPGA视频处理链路OV5640摄像头做图像采集FPGA内部完成图像预处理、车牌定位、字符分割与识别最后把识别结果通过OSD叠加到LCD显示。整个过程完全由硬件逻辑实现没有嵌入式处理器参与运算这也是FPGA图像处理最有魅力的地方——用流水线思维把一帧一帧的数据像工厂流水线一样处理掉。如果你正在学FPGA入门、准备做相关毕业设计或者想搞懂真正的硬件流水线是怎么工作的这篇文章应该能帮你省至少两个星期的摸索时间。1. 项目整体设计与方案选型思路1.1 核心需求拆解搞懂输入输出和中间的“黑箱子”拿到这个题目第一件事不是急着写Verilog而是把需求拆清楚。整个系统的输入是OV5640摄像头的实时视频流输出是LCD屏幕上的显示画面中间要做的事情是“识别车牌”。但识别这个动作听起来简单落在硬件上就变成了一连串问题采集到的彩色图像怎么转成适合处理的灰度图噪声怎么滤掉车牌区域怎么从整幅画面里找出来找到之后字符怎么切分、怎么认出来每一步在FPGA里都需要具体的硬件模块去实现而不是像软件里调一个OpenCV函数那么轻松。我最终把系统划分成几个大模块摄像头采集模块、图像预处理模块、车牌定位模块、字符分割识别模块、显示驱动模块以及贯穿始终的DDR3存储仲裁模块。模块化设计的好处是每个模块可以独立仿真、独立调试出了问题能快速定位。比如显示出来的图像颜色不对那大概率是采集或者显示的数据格式出了问题跟识别算法没关系如果识别率低那问题基本都集中在预处理和定位环节。这种拆解思路在纯软件项目里叫模块化编程在FPGA项目里就是硬件流水线设计的核心思维。1.2 器件选型为什么是OV5640、DDR3和模板匹配选型是整个项目最关键的决策点我换了三次方案才定下来。摄像头方面OV5640几乎是这个级别项目最平衡的选择500万像素CMOS传感器支持DVP并行接口和MIPI接口输出格式可以从RAW Bayer到RGB565再到YUV422随意配置而且价格不贵、资料多几乎每个FPGA开发板都标配这颗Sensor。相比之下OV7725虽然驱动更简单但最大只有30万像素做定位识别精度差一些OV2640虽然有200万像素但文档和社区资料不如OV5640全。更关键的是OV5640可以直接输出RGB565格式省去了在FPGA里做Bayer转RGB的ISP去马赛克流程这一步省掉的工程量相当可观。存储方案我纠结了很久。早期想用FPGA片上的BRAM做帧缓存因为代码最简单不用碰DDR控制器。但算了一笔账就放弃了一帧1280x720的RGB565图像需要1280x720x2约1.8MB的存储空间即便是黑金或小梅哥的中端开发板BRAM总量也就一两兆比特算下来连半帧都存不下。所以最终采用了DDR3方案用开发板上的DDR3颗粒做帧缓存虽然要处理AXI总线和读写仲裁但这是所有视频项目绕不开的技术点早晚要学。识别算法方面我选了模板匹配而不是神经网络原因很简单FPGA资源有限跑CNN需要大量的DSP和BRAM而经典车牌字符识别用模板匹配完全可以做到实用级别且每一步的中间结果都可以用ILA抓出来检查调试起来痛快得多。2. OV5640采集链路与DDR3缓存的设计细节2.1 SCCB寄存器配置摄像头“听话”的关键OV5640本身是一颗功能极其复杂的芯片上电之后默认输出的是自动测试图案必须通过SCCB总线兼容I2C协议写寄存器才能让它按照我们想要的分辨率、帧率和数据格式输出图像。SCCB的时序和I2C非常接近OV5640作为从机7位地址是0x3C写方向配置时通过SCCB接口逐寄存器写入。我推荐的做法是把寄存器配置表放到一个只读存储器里用一个状态机循环读出配置参数并通过SCCB发送配置完成后置一个高电平信号通知后续模块可以开始接收数据。这里有几个必须注意的坑。第一是配置顺序OV5640很多寄存器之间有依赖关系必须按厂商推荐的初始化序列来写比如先唤醒芯片再配置分辨率顺序反了会导致输出花屏或者干脆没有信号。第二是PLL设置输出像素时钟PCLK的频率由寄存器组决定如果你想跑720p30fpsPCLK大约需要在60MHz左右这个值要跟FPGA侧的采集逻辑时钟对齐否则采样会错位。第三是输出格式寄存器必须把格式设为RGB565而不是JPEG或YUV422因为我后续的灰度化模块是直接拿RGB分量做加权运算如果拿到的数据是压缩流就得先做解码工程量完全不是一个量级。2.2 CMOS采集模块把并行数据流变成可处理的图像帧OV5640配置完成后输出的信号包括像素时钟PCLK、行有效HREF、帧同步VSYNC和8位或10位的数据总线。以RGB565为例每个像素占16位摄像头会在两个PCLK周期内分别送出高8位和低8位数据因此FPGA采集模块的第一步就是把两个周期的数据拼成一个16位像素。拼完之后就是标准的视频同步逻辑检测VSYNC上升沿表示新的一帧开始HREF高电平时每个PCLK采样一次数据把行列坐标计数器跑起来同时把拼接好的RGB565像素写入FIFO。这个模块的时序敏感度非常高。PCLK是摄像头自己产生的属于完全独立的时钟域而FPGA内部逻辑跑在系统时钟上跨时钟域处理必须用异步FIFO不能直接把PCLK采到的数据给后面的处理模块用。我见过不少人栽在这个问题上图像偶发撕裂、画面有横条纹基本都是跨时钟域没处理好导致的亚稳态。异步FIFO的深度至少需要缓存一行以上的像素我用的是2048x16这样即使突发数据稍大也不会溢出。另外建议在VSYNC和HREF信号进入FPGA后先做两级寄存器同步打拍把亚稳态概率降到最低。2.3 DDR3帧缓存带宽计算、读写仲裁和行缓存策略图像经过采集之后会以帧为单位写入DDR3。为什么必须上DDR3而不直接在FPGA内部做流水线因为车牌定位和字符识别需要对整帧图像进行多次扫描比如行投影需要统计整行的边缘特征这要求数据能随机访问而不仅是顺序流出。DDR3在这里的作用就是提供一个“可以反复读取的帧仓库”。挑选DDR3方案时我先把带宽需求算清楚了。以1280x72030fps、RGB565格式计算每秒数据量为1280x720x2x30约等于每秒55MB。写入一路、读取一路再加上识别模块反复读取同一帧总带宽需求大概在150MB/s到200MB/s之间。DDR3的物理带宽通常在几个GB/s级别跑这种应用绰绰有余所以瓶颈不在于带宽而在于控制器效率。我设计里把读写请求分成几个口采集写端口、LCD读端口、识别算法读端口通过一个仲裁状态机按照优先级轮流服务。需要注意的是突发长度尽量用满比如每次突发读256字节充分利用DDR3的prefetch机制否则频繁的行切换会把有效带宽浪费掉一大半。实际测试下来帧率稳定在30fps没有任何问题。3. 牌照识别算法的FPGA硬件化实现3.1 彩色转灰度与3x3中值滤波细节决定后面好不好干车牌识别的第一步是图像预处理包括灰度化、滤波和二值化。灰度化直接用经典加权平均公式Y 0.299R 0.587G 0.114B。但在FPGA里做浮点乘法太奢侈正确做法是转成整数近似Y (R x 77 G x 150 B x 29) 8这里的乘法和移位消耗的都是DSP和逻辑资源但远远好过浮点运算。如果晚上还要处理低照度画面可以考虑查表或者先用直方图均衡增强但这个项目主要针对白天场景我就没有做动态增强只做了固定参数处理。滤波用3x3中值滤波清理椒盐噪声这个操作表面简单实际做起来有几个关键点。3x3窗口意味着你必须同时拿到上下三行同一列的数据这需要两个行缓存Line Buffer本质上是用BRAM构造延迟线。行缓存的宽度是单行像素数720p一行有1280个像素每个像素8位灰度单行缓冲就是1280x8bitBRAM资源消耗可控。窗口数据准备好之后对这9个像素做排序取中值排序网络最常用的是三分组比较法九个值先排三组再比较出中值整个流水线只需要3到4个时钟周期就能出结果。这段逻辑说难不难但组合逻辑路径不短我建议在中间插入寄存器打拍不然时序收敛会很痛苦。3.2 车牌定位Sobel边缘检测加投影法的工程化组合车牌定位是整个识别链路中准确率最敏感的环节也是我调试时间花得最多的地方。标准的做法是先用Sobel算子做垂直边缘检测因为车牌字符和底色之间、字符和字符之间都有密集的垂直边缘在图像中形成一片高能量区域。Sobel算子的卷积核非常简单Gx方向用[-1,0,1; -2,0,2; -1,0,1]Gy方向用[-1,-2,-1; 0,0,0; 1,2,1]对3x3窗口做乘加。这个计算在FPGA里甚至不需要DSP直接用移位和加法就可以实现因为卷积核里的系数都是2的幂次关系乘以2就是左移一位。边缘检测之后得到的是边缘强度图接下来要定位车牌所在的矩形区域。我采用的是水平投影加垂直投影的经典组合先在整幅灰度图上做水平方向投影统计每一行边缘点的数量车牌区域通常会出现一个明显的峰值区间对应车牌的上下边界锁定水平区间后在区间内做垂直投影找到字符排列最密集的列区间从而确定左右边界。这两步运算在FPGA里做起来非常高效只需要把边缘图逐行累加得到行计数数组再用滑动窗口找峰值区间即可。实际遇到的最大问题是背景复杂时误检比如车头格栅、路面标线也会产生高边缘密度区域。我在两个地方做了约束一是车牌的宽度高度比通常在2到4之间二是车牌区域内部有至少5到7个字符产生周期性边缘信号。加了这两个先验条件之后现场的误检率大幅下降。3.3 字符分割与模板匹配识别最后一步的硬功夫定位到车牌区域后需要把区域内的字符逐个切分并识别。车牌标准是7个字符其中第一位是汉字后面是字母和数字混排字符之间有相对固定的间隔。分割方法以垂直投影为主把车牌区域内的二值图像向水平方向投影字符笔画会产生非零计数字符间隙则投影值接近零根据这个周期性就能切出每个字符的边界。切分有几个工程细节要处理第一个字符汉字内部结构复杂容易出现投影不连续需要用连通域分析把断裂部分合并二是粘连字符问题如果边缘检测后的二值图有噪声导致字符笔画连在一起切分就会失败我后来在切分前先做了一次腐蚀操作把窄桥断开效果立竿见影。字符切出来后统一归一化成标准尺寸我用的模板大小是84x48。归一化就是最简单的最近邻缩放在硬件里可以通过行计数器映射到源图像的坐标实现不需要真正的插值运算。识别采用模板匹配我把常用字符汉字、字母、数字预先做成二值模板存进ROM识别时逐一计算当前字符与每个模板的相似度相似度最高的模板就是识别结果。相似度的度量用异或非运算来做即统计两幅二值图中相同位置相同值的像素个数这个过程在FPGA里无非是做逐位比较再累加纯流水线处理7个字符全部识别完只需要几毫秒。识别率上干净场景下接近百分之九十以上复杂背景和强光场景会掉一些但对于学习项目完全够用。3.4 定点数运算与资源优化FPGA算法落地的基本功整个算法链路里没有用任何浮点运算这是FPGA算法实现的基本功。前面说的灰度化、Sobel梯度和归一化涉及的所有非整数运算全部用定点数表示。定点的核心思想很简单把一个小数放大2的N次方倍变成整数参与运算最后结果再右移N位还原。比如灰度系数0.299放大256倍就是76.544四舍五入取77这样Y (77R 150G 29B) 8误差不到千分之一但硬件实现成本只是几个乘法器和移位器。如果某个系数是固定值比如模板匹配中的归一化因子还可以进一步用移位和加法近似把DSP资源省下来给其他模块用。资源优化方面我的经验是优先使用移位和查找表替代通用乘法。例如Sobel的卷积计算中乘2就是左移一位乘3就是左移一位加原值这些操作在FPGA里几乎不消耗额外逻辑。对于需要较大动态范围的中间量比如直方图统计用BRAM实现查找表比用LUT阵列省资源得多。FPGA的资源总是有限的做图像处理的时候要养成“每用一块DSP、每用一块BRAM之前都问一句有没有更省的办法”这样的习惯。4. LCD显示驱动与全系统集成4.1 显示通路设计识别结果是怎么“画”到屏幕上的LCD显示模块的工作分两层底层是把DDR3里的视频帧按照LCD的时序要求读出来刷新上层是把识别得到的字符信息叠加到一个图层上再做混合输出。我用的是常用的RGB接口TFT LCD支持行场同步和时钟信号分辨率1280x720刚好和摄像头分辨率一致不需要缩放。底层显示的原理跟CRT时代完全一样LCD面板逐行扫描每行从左到右逐点写入像素值一帧扫完后回到左上角开始下一帧。所谓显示驱动核心就是根据LCD的数据手册生成正确的时序波形行同步和场同步的周期、前后肩、有效数据区边界这些参数差一个时钟周期都会导致画面偏移或者滚动。这些时序参数通常可以查LCD数据手册得到然后在FPGA里用计数器精确产生。显示数据从DDR3读出来经过FIFO缓冲后按像素时钟依次送到LCD的数据总线。叠加层做的其实是一个简单的OSD混合。在显示模块里我把屏幕坐标和车牌定位坐标做比较如果当前像素落在车牌框内就输出一个高亮的框线模式如果落在字符显示区域内就根据识别结果ROM里预置的字符点阵把对应像素替换成白色或者黑色字形。这样一个“原始画面加识别框加字符结果”的叠加画面就完整呈现出来了。整个叠加过程只是几个坐标比较器和数据选择器逻辑开销非常小。4.2 时钟架构与跨时钟域牵一发动全身的地方系统里有好几个异步时钟域OV5640输出的像素时钟约60MHzFPGA系统主时钟50MHzDDR3控制器的工作时钟可以跑到400MHz以上LCD的像素时钟又是另一个频率。如果这些时钟域的数据不做妥善处理系统跑起来轻则偶发花屏重则直接死机。我的做法是尽量让每一条数据通路都通过异步FIFO来跨越时钟域摄像头PCLK域的数据进入DDR3控制器从DDR3读出的数据进入LCD像素时钟域都隔着FIFO传递数据数据请求信号单独用两级同步器打拍。异步FIFO深度根据读写速率差来定读写带宽接近时深度可以小但当读取是突发模式时深度要能覆盖一整个突发周期内写入的数据量我统一用了2048深度稳妥。复位信号的处理同样被很多人忽视。FPGA全局复位如果异步释放很容易让内部状态机进入非法状态也就是常说的复位信号亚稳态问题。我最后采用了“异步复位、同步释放”的标准电路复位信号进入后先经过两级同步器再通过一个专门产生的复位脉冲分布到各模块。这个电路能保证所有触发器在同一个时钟沿脱离复位不至于出现一部分模块已经工作、另一部分还在复位的错乱状态。4.3 仿真到板级联调没有ILA这个项目根本调不出来联调阶段最大的体会是仿真可以验证模块逻辑正确但真正的问题几乎都在接口时序和数据格式上必须到板子上抓实际信号才能发现。整个联调我采用让识别模块先置为旁路模式也就是什么都不识别直接把采集的图像写入DDR3再让LCD读出来。这一步能验证摄像头、DDDR控制器和LCD三条链路的正确性。如果屏幕能稳定显示清晰的实时画面说明底层通路已经通了。然后才打开识别模块用片内逻辑分析仪去抓预处理模块的关键信号。这里我的调试思路是抓三个地方一是灰度化输出的数据看图像内容是否正确而不是花屏二是Sobel边缘图确认车牌位置的边缘是否形成明显的高亮区域三是投影数组的波形看峰值是否落在预期的行和列范围。ILA触发的条件我设置为检测到VSYNC有效后的指定行和指定列这样抓到的数据一定是画面中间特定位置的值。一步步确认每个模块的数据是对的最后总体的识别结果自然就是对的。这个过程虽然繁琐但比出了错对着整段代码瞎猜要高效得多。5. 常见问题、调试心得与排障速查5.1 典型问题速查表照着排查效率翻倍项目做到后面我整理了一张问题速查表遇到现象就能直接定位到环节。花屏或图像错位基本是LCD时序参数配错检查行场同步和前后肩画面有横条纹或撕裂大概率是跨时钟域FIFO深度不够或者DDR3读写仲裁冲突颜色不对八成是RGB565高低字节顺序弄反这个错误我在采集模块和数据拼接时都犯过图像太暗或太亮先看OV5640曝光和增益寄存器配置再看灰度化系数是否合理。下面这张表是我最后整理出的排障清单现象可能原因排查方向LCD无显示或全花LCD时序参数配置错误对照LCD数据手册检查行场同步、前后肩画面撕裂/横条纹DDR3读写冲突或FIFO深度不足降低帧率测试确认跨时钟FIFO不溢出图像色彩不对RGB565字节序错误互换高8位和低8位拼接顺序画面有漂移/滚动LCD垂直同步参数不对检查VSYNC周期和有效行数识别率突然下降二值化阈值不适应光照变化改用自适应阈值或固定阈值多加光照约束偶发死机或状态异常复位处理不当改成异步复位同步释放电路DDR3读写效率低突发长度不足或行切换频繁提高单次读写长度优化仲裁优先级5.2 时序收敛与布局布线经验整个设计综合实现的时候也遇到过程序没问题但跑不稳定的情况。后来总结主要是几个关键时序路径没有约束好。OV5640的PCLK输入引脚必须要用create_clock约束告诉工具这个时钟是外部输入DDR3控制器的时钟组要单独约束并且把物理位置相关引脚用XDC管脚约束文件锁好跨时钟域的异步FIFO是安全路径需要set_clock_groups -asynchronous告诉时序工具不要在这条路径上做时序检查否则跑implementation时会因为虚假的违例路径导致布线失败。布局布线时遇到的一个经典问题是识别算法模块组合逻辑路径太长导致时序不收敛。我的解决方法是把Sobel的三个方向梯度的计算拆成两级中间插寄存器打拍这样最大路径长度缩短了差不多一半时序马上就收敛了。另外一个有用的小技巧是给局部高扇出信号比如复位加max_fanout约束工具会自动复制寄存器减轻单一驱动点的负载压力。5.3 模板生成工具与仿真技巧制作字符模板的过程与其手工写数据不如借助工具。字符点阵可以先用脚本把常用字库渲染成二值图比如用Python PIL在84x48分辨率下输出每个字符的点阵再转成COE文件格式供ROM初始化。还有一种思路是直接用LCD Image Converter这类工具把字体图片转换成Verilog可以读取的初始化文件或者导出成RTL初始化数组。这种方法比自己手算点阵效率高一个数量级还不会出错。仿真验证阶段有两点提示一是OV5640的行为模型不存在仿真时我用的是自定义的Testbench来模拟摄像头输出构造一个固定的测试画面包含模拟的车牌区域这样预处理和定位模块可以先用仿真跑通再上板二是DDR3控制器的仿真需要厂商的内存模型但也不是绝对必要可以先给上层逻辑写一个简化的行为级存储模型把格式转换和角度看对然后再接真控制器这类做法能省下大量仿真时间。写在最后的几句大实话这套系统跑通的那一刻屏幕上稳定显示出实时画面并叠加出识别结果说实话那种成就感比写完多少个软件程序都要强烈。回头看整个过程最大的收获不是具体某段代码而是建立了一种硬件流水线的思维方式数据在一个个模块间像水流一样流过每个模块只干一件事所有模块同时工作。这种思维方式确实是纯软件背景的人很难直接体会到的。再做这个项目有几点想给后来人分享。第一先确保底层图像通路完全稳定再开始做算法底层不稳算法调多久都是白费。第二每次只改一个点不要一次性改多个模块再上板测试不然出了问题根本不知道是哪一处改出来的。第三信号一定要用ILA抓实际波形确认不要盲目相信仿真结果。第四给自己留足调试时间FPGA项目的调试时间是写代码时间的三倍以上这很正常。如果想把识别率再做高一些可以考虑两个方向一是把固定二值化阈值改成自适应阈值对光照变化的鲁棒性会好很多二是在定位环节引入颜色特征也就是先按车牌的蓝色或黄色像素做颜色过滤再结合边缘信息定位。这些都是硬件可以实现的方向做起来时间成本不高但对效果的提升很明显。总之这个项目确实值得折腾做完这套之后再去看FPGA图像处理的其他应用底层套路基本都是相通的。