FPGA车牌识别系统:Verilog硬件流水线设计与工程实践
2026/9/1 1:04:27 网站建设 项目流程

简介:本资源是一套基于FPGA实现的车牌识别系统完整工程,面向数字电路、嵌入式视觉与计算机视觉方向的学习者与开发者,解决图像处理算法硬件化落地的关键实践问题。压缩包共含多个文件,以Verilog源代码、Quartus II工程文件、系统架构图(PNG)、详细设计报告(PDF/DOC)及实机上板演示视频(MP4)为主,涵盖图像采集、预处理、车牌定位、字符分割与识别等全流程模块的RTL级实现,总大小18.34MB。目前已有1166人学习下载,体现了其在FPGA图像应用教学与课程设计中的高实用价值。用户可直接导入Quartus II编译下载至主流Altera/Intel FPGA开发板运行验证,配套报告详述设计原理、模块划分、时序约束与性能分析,视频直观展示实时识别效果,是深入理解硬件加速视觉算法不可多得的闭环学习案例。

1. 项目背景与核心价值:从“黑盒”到“白盒”的硬件车牌识别

最近在整理硬盘时,翻出了一个老项目压缩包,文件名是“车牌识别verilog_quartusII_带录制视频_带报告.rar”。这个压缩包就像是一个时间胶囊,里面封存了一段典型的数字系统设计学习历程。对于很多电子工程、FPGA(现场可编程门阵列)方向的同学来说,车牌识别项目几乎是迈入“算法硬件化”大门的第一块敲门砖。它不像调用一个现成的OpenCV库函数那么简单,而是要求你从最底层的像素操作开始,用硬件描述语言(如Verilog)去构建一个完整的图像处理流水线,最终在FPGA开发板上实时输出识别结果。这个项目之所以经典,是因为它串联了数字图像处理、数字逻辑设计、FPGA开发工具链使用以及系统集成测试等多个核心技能点。

市面上有很多成熟的车牌识别软件方案,基于YOLO等深度学习框架的算法精度高、适应性强。但为什么我们还要用Verilog在FPGA上“从头造轮子”呢?这背后的核心价值在于“确定性”和“效率”。软件方案运行在通用处理器(CPU/GPU)上,其执行时间是波动的,受操作系统调度、内存访问延迟等因素影响。而用Verilog设计的硬件电路,一旦烧录到FPGA中,其处理每一帧图像的时钟周期数是固定的,实现了严格的实时性。这对于一些对时序有苛刻要求的工业视觉场景(如高速流水线检测)至关重要。此外,将算法固化到硬件中,可以摆脱对高性能计算平台的依赖,实现低功耗、高可靠性的嵌入式解决方案。

这个“带录制视频_带报告”的项目包,其完整意义在于它不仅仅是一段代码(.v文件),更是一个可复现的工程闭环。它包含了用于逻辑综合与布局布线的Quartus II工程文件、可能用于仿真的Testbench、记录实际运行效果的视频、以及阐述设计思路和结果的分析报告。对于学习者而言,拥有这样一个完整包,意味着你可以跳过最令人头疼的环境搭建和框架设计,直接切入核心算法的硬件实现与优化,理解从算法到硬件的映射过程。接下来,我将以这个项目包为蓝本,拆解一个基于Verilog和FPGA的实时车牌识别系统的完整设计与实现细节,分享其中关键的技术抉择、容易踩坑的环节以及从仿真到上板的调试心得。

2. 系统架构设计:构建图像处理流水线

一个完整的车牌识别硬件系统,其核心是一个精心设计的图像处理流水线。这条流水线的输入是摄像头采集的原始RGB图像数据流,输出是识别出的车牌字符文本。整个流程必须在像素时钟的驱动下,像工厂流水线一样,每个环节处理上一个环节的输出,并产生给下一个环节的输入,以此实现实时处理。

2.1 顶层模块划分与接口定义

首先,我们需要用Verilog定义一个清晰的顶层模块(Top Module),它就像系统的总蓝图,规定了各个子模块如何连接以及如何与外部世界(摄像头、显示器、存储器等)通信。

module license_plate_recognition_top ( input wire clk, // 系统主时钟,例如50MHz input wire rst_n, // 低电平有效的全局复位信号 // 摄像头输入接口 (假设为RGB565格式) input wire cam_vsync, // 摄像头场同步信号 input wire cam_href, // 摄像头行有效信号 input wire cam_pclk, // 摄像头像素时钟 input wire [7:0] cam_data, // 摄像头数据(8位,需拼接为16位RGB565) // VGA显示输出接口 output wire vga_hsync, output wire vga_vsync, output wire [4:0] vga_r, // VGA通常为5-6-5位RGB output wire [5:0] vga_g, output wire [4:0] vga_b, // 串口输出接口(用于输出识别结果) output wire uart_tx );

在这个顶层模块中,我们实例化几个核心子模块:

  1. 图像采集与缓存模块:负责在cam_pclk域下接收摄像头数据,并将其写入一个FIFO(先进先出存储器)或双端口RAM中,完成从摄像头时钟域到系统主时钟域(clk)的跨时钟域处理。这是第一个容易出问题的地方,如果异步FIFO的深度设计不当,会导致图像数据丢失或溢出。
  2. 图像预处理流水线:这是算法的核心部分,运行在主时钟域。通常包括:
    • 灰度化:将RGB图像转换为单通道的灰度图像,减少后续处理的数据量。Gray = 0.299*R + 0.587*G + 0.114*B,在硬件中我们常用移位和加法来近似这个公式,例如Gray = (R>>2) + (R>>5) + (G>>1) + (G>>4) + (B>>3),以节省乘法器资源。
    • 高斯滤波/滑动窗口滤波:用于平滑图像,抑制噪声。这里就需要实现一个滑动窗口,例如3x3的窗口。对于窗口内的9个像素,使用预先计算好的系数进行加权求和。关键点:需要设计一个行缓冲器(Line Buffer),通常由两个RAM或移位寄存器实现,用于缓存前两行的图像数据,以便与当前行数据共同构成3x3窗口。
    • 边缘检测(如Sobel算子):突出车牌的边缘。同样需要3x3窗口,分别计算水平和垂直方向的梯度,然后求模。在硬件中,求平方和开根号开销大,常用绝对值求和来近似:G = |Gx| + |Gy|
    • 二值化:将灰度或梯度图像转换为黑白二值图,便于后续形态学操作。阈值的选择至关重要,可以采用固定阈值、全局OTSU阈值(计算量大)或局部自适应阈值。
  3. 车牌定位模块:在二值化图像中寻找可能是车牌的矩形区域。常用方法包括:
    • 形态学操作:通过腐蚀、膨胀、闭运算等连接车牌字符区域,形成一个连通域。
    • 轮廓查找与筛选:通过扫描图像,标记连通域,然后根据车牌的先验知识(长宽比、面积、占空比等)筛选出候选区域。这个模块会输出车牌区域的坐标(左上角x,y,宽度,高度)。
  4. 字符分割与识别模块:对定位到的车牌区域进行进一步处理。
    • 字符分割:将车牌区域图像进行垂直投影,根据投影的波峰波谷切分出单个字符。这里要处理字符间隔、粘连字符(如“京”和“A”可能连在一起)等问题。
    • 字符识别:对分割出的单个字符进行识别。在资源有限的FPGA上,实现完整的OCR(光学字符识别)神经网络比较困难。因此,这个老项目很可能采用模板匹配法。即预先存储标准字体(0-9, A-Z,部分汉字)的二值化模板,然后将待识别字符的二值化图像与所有模板进行比对(如计算汉明距离或相关系数),取距离最小的模板作为识别结果。这种方法简单、速度快,但对字体、光照变化敏感。
  5. 显示与控制模块:负责将原始图像、处理中间结果或识别结果叠加显示在VGA显示器上,并通过UART将识别出的车牌号发送给上位机(PC)保存或进一步处理。

注意:在Quartus II中组织这些模块时,务必理清编译顺序。将底层模块(如FIFO、RAM、滤波器核)先加入工程,再添加上层调用模块。否则在分析综合时可能会报找不到模块定义的错误。

2.2 关键数据结构:行缓冲器与窗口生成器

图像处理算法的硬件实现,其效率瓶颈往往在于数据访问。以3x3滑动窗口滤波为例,我们不能为了处理当前像素P(x,y),就去存储器里随机访问P(x-1,y-1),P(x,y-1)... 这会造成巨大的访问延迟和带宽压力。

正确的做法是使用行缓冲器。我们使用两个寄存器组或小块RAM(Buffer0, Buffer1),每个能存储一行的像素数据。系统工作流程如下:

  1. 第N行数据到来时,存入Buffer0,同时将Buffer0的数据输出给处理单元作为“上一行”。
  2. 第N+1行数据到来时,存入Buffer1,同时将Buffer1的数据输出作为“当前行”,Buffer0的数据作为“上一行”。
  3. 第N+2行数据到来时,数据再次存入Buffer0,覆盖旧数据,此时Buffer0输出为“下一行”,Buffer1输出为“当前行”。 如此循环,我们就在每个时钟周期,都能同时获得同一列的三个连续行像素,再配合一个3级的移位寄存器(用于缓存当前行的前两个像素),就能在每个像素时钟周期,动态地组装出一个完整的3x3窗口[P22, P23, P24; P32, P33, P34; P42, P43, P44],供给滤波器使用。这种设计确保了数据流的连续性,是硬件图像处理的基础范式。

3. 核心算法模块的Verilog实现细节

有了顶层架构,我们深入到几个关键算法的硬件实现中,这里藏着许多从理论公式到实际电路的“魔鬼细节”。

3.1 滑动窗口滤波器的实现与优化

我们以实现一个3x3的均值滤波(或高斯滤波)为例。均值滤波的公式很简单:窗口内9个像素值求和,然后除以9。但在硬件里,除法(尤其是非2的幂次方的除法)是昂贵的操作。

module sliding_window_filter_3x3 ( input wire clk, input wire rst_n, input wire [7:0] pixel_in, // 输入的灰度像素流 input wire pixel_in_valid, // 输入像素有效信号 output reg [7:0] pixel_out, // 滤波后的像素输出 output reg pixel_out_valid ); // 行缓冲器:用两个双端口RAM或移位寄存器实现 reg [7:0] line_buffer_0 [0:IMG_WIDTH-1]; reg [7:0] line_buffer_1 [0:IMG_WIDTH-1]; // 窗口寄存器:存储3x3窗口的9个像素值 reg [7:0] win_11, win_12, win_13; // 上一行 reg [7:0] win_21, win_22, win_23; // 当前行(中心为win_22) reg [7:0] win_31, win_32, win_33; // 下一行 // 列计数器与行计数器,用于控制数据填充和窗口滑动 reg [10:0] col_cnt; reg [10:0] row_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin // 复位计数器、缓冲器等 col_cnt <= 0; row_cnt <= 0; // ... 其他寄存器复位 end else if (pixel_in_valid) begin // 1. 数据流进入,更新行缓冲器 line_buffer_1[col_cnt] <= pixel_in; // 当前行存入buffer1 win_31 <= line_buffer_0[col_cnt]; // buffer0的数据作为“下一行”进入窗口 win_21 <= line_buffer_1[col_cnt]; // buffer1的旧数据作为“当前行” win_11 <= line_buffer_0[col_cnt]; // 需要另一个缓冲?这里逻辑简化,实际需更精细控制 // 2. 横向滑动窗口:使用移位寄存器链 win_13 <= win_12; win_12 <= win_11; win_23 <= win_22; win_22 <= win_21; win_33 <= win_32; win_32 <= win_31; // 3. 当窗口填满(col_cnt >= 2且 row_cnt >= 2)时,计算滤波结果 if (col_cnt >= 2 && row_cnt >= 2) begin // 求和 wire [10:0] sum = win_11 + win_12 + win_13 + win_21 + win_22 + win_23 + win_31 + win_32 + win_33; // 除以9:因为9=8+1,可以近似为 sum/8 - sum/64 + ...,或者直接用查找表 // 简单处理:pixel_out = sum[10:3]; (即除以8,会有误差但速度快) pixel_out <= sum[10:3]; // 相当于右移3位 pixel_out_valid <= 1'b1; end else begin pixel_out_valid <= 1'b0; end // 4. 更新列计数器,一行结束时更新行计数器并交换行缓冲器 if (col_cnt == IMG_WIDTH - 1) begin col_cnt <= 0; row_cnt <= row_cnt + 1; // 交换line_buffer_0和line_buffer_1的角色 // 实际是通过读写地址控制来实现,这里概念性交换 end else begin col_cnt <= col_cnt + 1; end end end endmodule

实操心得:上面代码是一个高度简化的概念模型。实际工程中,行缓冲器通常用FPGA内部的Block RAM实现,并通过读写地址指针精确控制。一个常见的坑是边界处理:对于图像边缘的像素,其3x3窗口不完整。通常有两种策略:一是复制边缘像素(padding),二是不处理边缘像素,导致输出图像尺寸略小。需要在设计初期统一约定。

3.2 二值化与车牌定位的逻辑设计

二值化模块接收滤波后的灰度图像流,输出二值图像流。如果采用固定阈值,实现非常简单:binary_out = (gray_in > THRESHOLD) ? 1‘b1 : 1’b0。但固定阈值对环境光线变化敏感。

更鲁棒的方法是局部自适应阈值,比如计算当前像素所在局部区域(如15x15窗口)的灰度均值,以此作为阈值。这意味着我们需要在二值化模块前,再级联一个用于计算局部均值的滑动窗口滤波器,这会增加逻辑资源和延迟。在资源紧张的设计中,需要权衡。

车牌定位模块是算法成败的关键。它工作在二值图像流上。一种实用的硬件友好算法是连通域标记与矩形拟合的简化版:

  1. 水平投影与垂直投影:我们不需要存储整幅二值图像。可以实时计算每一行和每一列的白色像素(值为1)的累加和。当扫描完一帧图像后,我们就得到了两个一维数组:水平投影曲线和垂直投影曲线。
  2. 寻找波峰:车牌的矩形区域在投影曲线上会表现为一个明显的波峰。通过设置一个阈值,找到水平投影上连续超过阈值的行区间(候选车牌高度),以及垂直投影上连续超过阈值的列区间(候选车牌宽度)。
  3. 几何筛选:对找到的候选矩形,根据预设的车牌长宽比范围(例如中国车牌约为3.14:1)、最小/最大面积进行筛选。这个筛选逻辑可以用比较器(Comparator)和乘法器(计算长宽比)轻松实现。

这个方法的优点是无需存储整帧图像,节省内存,且计算量小,非常适合流水线处理。缺点是对于倾斜、弯曲的车牌效果会变差。

3.3 基于模板匹配的字符识别

字符识别模块接收定位模块切割出的单个字符图像块(例如20x40像素的二值图)。模板匹配法的Verilog实现核心是一个距离计算与比较电路

假设我们有36个模板(数字0-9,字母A-Z),每个模板也是20x40的二值图。识别过程如下:

  1. 并行化:为了速度,我们可以实例化36个相同的“距离计算单元”,每个单元负责计算待识别字符与一个模板的距离。
  2. 距离计算:最常用的是汉明距离(Hamming Distance),即两个二值图像对应位不同的数量。对于二值图,这等价于进行位异或(XOR)操作,然后统计结果为1的位数(即popcount操作)。
    // 单个距离计算单元示例(假设字符已展开为800位的向量) module template_matcher_unit ( input wire [799:0] char_bits, // 待识别字符 input wire [799:0] tmpl_bits, // 模板字符 output reg [9:0] hamming_dist // 汉明距离 ); wire [799:0] diff_bits; assign diff_bits = char_bits ^ tmpl_bits; // 按位异或 // 计算diff_bits中1的个数(popcount) always @(*) begin // 这是一个组合逻辑,实际实现可能需要多级加法树以优化时序 integer i; reg [9:0] count; count = 0; for (i=0; i<800; i=i+1) begin if (diff_bits[i]) count = count + 1; end hamming_dist = count; end endmodule
  3. 最小值查找:36个距离计算单元同时输出36个距离值。我们需要一个比较树(Comparator Tree)来找出其中的最小值,并记录其对应的模板索引。这个索引就对应了识别出的字符(0-9, A-Z)。
  4. 模板存储:36个800位的模板需要存储在FPGA的ROM(只读存储器)中。在Quartus II中,我们可以用.mif(Memory Initialization File)文件来初始化ROM的内容,这些.mif文件由MATLAB或Python脚本根据标准字体图片预先生成。

踩坑记录:模板匹配对字符的归一化(大小、位置)要求很高。如果定位切割模块输出的字符图像有轻微偏移或缩放,匹配距离就会很大,导致误识别。因此,在字符分割后,必须增加一个归一化子模块,将切割出的字符图像缩放到与模板完全相同的尺寸(如20x40),并使其居中。这个缩放操作在硬件中可以用最近邻插值法实现,虽然会损失一些质量,但计算简单。

4. Quartus II工程管理、仿真与上板调试

拥有Verilog代码只是第一步,将其变成能在FPGA上运行的电路,还需要经过综合、布局布线、编程等步骤,而Quartus II正是完成这些工作的集成环境。

4.1 工程创建与约束文件编写

在Quartus II中新建工程时,选择正确的FPGA器件型号至关重要(例如Cyclone IV EP4CE10)。这决定了后续可用的逻辑资源、存储资源和IO引脚数量。添加所有Verilog源文件后,最重要的一步是编写引脚分配约束文件(.qsf文件或通过GUI分配)。

约束主要包括两类:

  1. 引脚位置约束:将顶层模块的输入输出信号(clk,rst_n,cam_*,vga_*,uart_tx)分配到FPGA开发板上实际物理引脚。必须查阅开发板的原理图。例如:
    set_location_assignment PIN_E1 -to clk set_location_assignment PIN_M1 -to rst_n set_location_assignment PIN_G1 -to uart_tx
  2. 时序约束:告诉时序分析工具时钟信号的频率,这是保证设计稳定运行的关键。通过.sdc文件定义:
    create_clock -name sys_clk -period 20.000 [get_ports clk] # 50MHz时钟

一个巨坑:摄像头接口(如OV7670)的像素时钟cam_pclk和系统主时钟clk是异步的。直接使用cam_pclk去驱动采样cam_data,然后试图在clk域使用这些数据,会导致亚稳态。必须使用异步FIFO进行跨时钟域处理。在Quartus II中,可以直接调用IP Catalog中的“FIFO”IP核,将其配置为异步模式(Independent Clocks),写时钟接cam_pclk,读时钟接clk。深度要设置合理,通常能存储若干行图像数据为宜。

4.2 功能仿真与Modelsim协同

在烧录到板子之前,必须进行充分的仿真。Quartus II自带的仿真工具功能较弱,通常与ModelSim协同使用。

  1. 编写Testbench:创建一个Verilog测试文件(tb_license_plate.v),在其中实例化你的顶层模块。Testbench的任务是模拟真实环境:

    • 生成系统时钟clk和复位信号rst_n
    • 模拟摄像头数据流:按照cam_vsync,cam_href,cam_pclk的时序,从文本文件或数组中将图像数据(可以用MATLAB将图片转为十六进制文本)赋值给cam_data。这个过程要严格遵循所用摄像头的时序图。
    • 监视输出信号,如uart_tx,检查发送的数据是否正确识别出了车牌号。
  2. 联合仿真:在Quartus II中设置仿真工具为ModelSim,并编译仿真库。然后运行RTL仿真。在ModelSim的波形窗口中,你可以清晰地看到信号的变化,检查图像数据是否被正确采集、预处理流水线每个阶段的输出、定位模块是否输出了正确的坐标、字符识别模块最终输出的UART字节是否符合预期。

调试技巧:在Testbench中,可以将关键中间信号(如二值化后的图像、定位坐标)写入文件。然后使用MATLAB或Python脚本读取并显示,直观地判断算法在哪个环节出了问题。这比看波形图要高效得多。

4.3 上板调试与问题排查

当仿真通过后,就可以将编译生成的.sof文件通过JTAG下载到FPGA开发板了。上板调试是“见证奇迹”也是“排查噩梦”的时刻。

  1. 无图像显示:首先检查VGA接口。确保引脚分配正确,VGA的行同步和场同步时序符合标准(可以先用一个简单的彩条发生器测试VGA模块是否正常)。如果VGA显示一片混乱,很可能是图像缓存器的读写地址逻辑有错误,导致显示错位。

  2. 图像有但处理结果不对

    • 图像颜色异常:检查RGB到灰度的转换公式是否正确,特别是位宽处理是否导致数据溢出或截断。
    • 滤波效果模糊或异常:检查滑动窗口滤波器的边界处理逻辑。可能是行缓冲器更新逻辑错误,导致窗口内的像素不是预期的空间相邻像素。
    • 车牌定位不到:这是最常见的问题。首先,通过VGA将二值化后的图像输出到显示器,观察在车牌区域是否形成了良好的白色连通域。如果二值化效果不好(太亮或太暗导致车牌区域与背景区分不开),需要调整二值化阈值。其次,检查定位模块的投影计算和阈值判断逻辑,可能需要将阈值参数做成可通过板载按键调节的寄存器,方便动态调试。
  3. 字符识别错误率高

    • 检查字符分割:通过VGA在原始图像上画出分割线,观察分割是否准确。垂直投影法对车牌倾斜很敏感。如果车牌有倾斜,需要先进行倾斜校正。一个简单的硬件校正方法是计算车牌的上下边界,根据其倾斜角度对切割出的区域进行插值重排,但这会显著增加设计复杂度。
    • 检查模板匹配:确认ROM中的模板数据是否正确加载。可以在Testbench中直接读取ROM内容与预期值对比。检查字符归一化模块,确保切割出的字符被正确地缩放和居中到20x40的网格中。
  4. 系统不稳定(随机错误)

    • 首要怀疑时钟和复位:用示波器测量板载晶振给FPGA的时钟是否干净。检查复位电路,确保上电后复位信号有足够长时间的稳定低电平。
    • 检查跨时钟域:确认摄像头数据到系统时钟域的异步FIFO是否被正确例化和连接。查看Quartus II的时序分析报告(TimeQuest),确保没有建立时间(Setup Time)或保持时间(Hold Time)违例。如果有违例,可能需要优化代码或添加时序约束。

录制视频与报告撰写:项目包中的“带录制视频”通常指用手机或摄像机拍摄FPGA开发板连接显示器实时识别车牌的演示过程。而“报告”则应系统性地阐述:项目需求、整体方案设计、各模块详细设计(框图、状态机、关键代码解释)、仿真波形与分析、上板实测结果(附照片或视频截图)、遇到的问题与解决方案、以及总结与展望。一份好的报告本身就是对项目的再次梳理和升华。

通过这样一个从理论到代码,从仿真到实物的完整流程,你收获的不仅仅是一个车牌识别系统,更是一套应对复杂数字系统设计的工程方法论。每一个信号、每一行代码、每一次约束,都直接对应着硬件电路的行为,这种掌控感是软件编程无法比拟的。虽然现在深度学习在车牌识别上效果更好,但理解这套经典的、确定性的硬件处理流程,对于从事高性能计算、边缘AI加速、实时控制系统等领域,依然是无比宝贵的底层经验。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询