1. 项目概述:基于FPGA的车牌识别系统实战
去年用正点原子达芬奇Pro100t开发板折腾了一个车牌识别系统,从摄像头采集到字符识别全流程都在FPGA上实现。这个项目最让我兴奋的是,整个处理流水线的延迟只有8个时钟周期,挂上HDMI显示器能看到实时处理效果。系统在阴天环境下的识别率能达到85%,夜间开启补光灯后提升到92%,对于纯硬件方案来说已经相当不错。
整个工程基于Vivado 2018.3开发,主要包含五大核心模块:RGB转YUV色彩空间转换、Sobel边缘检测、形态学处理(腐蚀/膨胀)、动态阈值特征提取以及卷积模板匹配。所有自定义IP核都保留了创建过程的截图,方便后续移植到其他Xilinx FPGA平台。实测资源占用率控制在合理范围(LUT 37%,BRAM 62%),时序收敛在480MHz。
提示:虽然工程是基于Artix-7芯片(XC7A100T)开发,但通过参数化设计,移植到Zynq或其他Xilinx系列FPGA只需修改时钟约束和存储接口。
2. 核心算法实现与硬件优化
2.1 色彩空间转换的流水线设计
摄像头采集的RGB565数据需要先转换为YUV色彩空间。在PC上这可能就是几行代码的事,但在FPGA上实现需要考虑时序对齐和流水线优化。我用Vivado HLS生成的转换模块核心算法如下:
cpp复制Y = (76 * R + 150 * G + 29 * B) >> 8;
U = (-43 * R - 85 * G + 128 * B) >> 8 + 128;
V = (128 * R - 107 * G - 21 * B) >> 8 + 128;
硬件实现时采用了三级流水线结构:
- 第一级:乘法运算(使用DSP48E1单元)
- 第二级:加法树合并
- 第三级:移位和饱和处理

这个设计中最关键的是在HLS中设置#pragma HLS PIPELINE II=1指令,确保每个时钟周期都能处理一个新像素。实测在150MHz时钟下,处理1080p视频流毫无压力。
2.2 Sobel边缘检测的硬件加速
边缘检测采用了改进的Sobel算子,X/Y方向的卷积核配置如下:
python复制# X方向
[-1, 0, 1,
-2, 0, 2,
-1, 0, 1]
# Y方向
[-1,-2,-1,
0, 0, 0,
1, 2, 1]
硬件实现时遇到了数据溢出的问题。最初直接使用DSP48做累加,发现当相邻像素值差异较大时,累加结果会超出16位表示范围。后来改进的方案是:
- 对输入像素做符号位扩展(8位无符号转9位有符号)
- 卷积计算保留全部中间精度
- 最终结果截取有效位并做饱和处理

这个模块还复用了3x3滑动窗口生成器,该组件后来也被形态学处理和模板匹配模块共享,节省了约15%的LUT资源。
3. 形态学处理与特征提取
3.1 参数化形态学操作实现
车牌区域增强采用了膨胀+腐蚀的组合操作,Verilog实现的核心代码如下:
verilog复制always @(posedge clk) begin
if(en) begin
// 膨胀操作:3x3窗口内任一像素为1则输出1
dilate_out <= (window[0][0] | window[0][1] | ... | window[2][2]);
// 腐蚀操作:3x3窗口内所有像素为1才输出1
erode_out <= (window[0][0] & window[0][1] & ... & window[2][2]);
end
end
经过多次实测发现:
- 结构元素大小不宜超过3x3,否则时序难以收敛
- 五次膨胀后接三次腐蚀的效果最佳
- 每个形态学操作之间需要插入行缓存,确保图像边界处理正确
注意:膨胀和腐蚀操作的顺序很重要。先膨胀可以填充小孔洞,后腐蚀可以消除毛刺,这个组合被称为"闭运算",特别适合处理车牌区域的连通域。
3.2 动态阈值特征提取
传统固定阈值二值化在不同光照条件下效果不稳定。我设计了一个基于直方图分析的动态阈值模块:
verilog复制// 直方图统计模块
always @(posedge clk) begin
if(hist_we) begin
hist_ram[bin_addr] <= hist_ram[bin_addr] + 1;
end
end
// 阈值计算状态机
always @(posedge clk) begin
case(state)
IDLE: if(start) begin
total_pixels <= 0;
state <= ACCUMULATE;
end
ACCUMULATE: begin
// 计算像素总数和累积分布
if(hist_index == 255) state <= FIND_THRESH;
end
FIND_THRESH: begin
// 寻找直方图谷底作为阈值
if(hist_diff[hist_index] > hist_diff[hist_index+1])
threshold <= hist_index;
end
endcase
end
这个模块使用双端口Block RAM实现直方图统计,通过寻找像素值分布的"谷底"自动确定二值化阈值。实测比固定阈值法的识别率提升了约8%。
4. 模板匹配与系统集成
4.1 基于汉明距离的字符识别
字符识别环节预存了各省份简称的12x24点阵字模,采用汉明距离(异或运算)代替传统的相关运算:
verilog复制// 汉明距离计算
for(i=0; i<CHAR_HEIGHT; i=i+1) begin
for(j=0; j<CHAR_WIDTH; j=j+1) begin
hamming_dist = hamming_dist +
(template[i][j] ^ image_patch[i][j]);
end
end
这种实现方式有两个优势:
- 异或操作可以用单个LUT实现,比乘法器节省资源
- 在Xilinx FPGA上,每个SLICE可以并行处理多个LUT,实测速度比DSP方案快3倍
4.2 系统级集成与优化
整个系统的数据流如下:
code复制OV5640摄像头 → RGB2YUV → Sobel边缘检测 → 形态学处理 →
动态阈值 → 字符匹配 → HDMI显示
关键性能指标:
- 资源占用:LUT 37%,BRAM 62%,DSP 28%
- 最大时钟频率:480MHz(实际运行在150MHz)
- 处理延迟:8个时钟周期(@150MHz ≈ 53ns)
- 识别准确率:白天85%,夜间92%(需补光灯)

移植到其他开发板时需要注意:
- 修改
xdc约束文件中的时钟定义 - 调整Video DMA的帧存地址范围
- 根据摄像头接口修改MIPI CSI-2配置
5. 常见问题与调试技巧
5.1 图像采集异常排查
现象:HDMI显示画面出现条纹或卡顿
可能原因:
- DDR3控制器未正确初始化
- VDMA的帧缓冲地址冲突
- 像素时钟与数据时钟不同步
解决方案:
tcl复制# 在Vivado TCL控制台执行
reset_run impl_1
launch_runs impl_1 -to_step write_bitstream
5.2 时序收敛问题
现象:实现阶段报时序违例
优化方法:
- 对关键路径添加寄存器
- 使用
MAX_FANOUT属性限制信号扇出 - 对跨时钟域信号添加
ASYNC_REG约束
verilog复制(* ASYNC_REG = "TRUE" *) reg [7:0] sync_regs [0:1];
5.3 资源利用率优化
当BRAM利用率较高时,可以:
- 将部分查找表改用分布式RAM实现
- 共享行缓存(如边缘检测和形态学处理共用同一组行缓存)
- 降低图像处理分辨率(从1080p降到720p可节省50%存储)
6. 工程移植与扩展建议
这套系统虽然是在达芬奇Pro100t开发板上实现的,但通过参数化设计可以方便地移植到其他Xilinx平台。最近我在Zynq-7020上做了移植测试,主要改动包括:
- 将DDR3控制器替换为PS端的HP端口
- 增加AXI DMA用于PS-PL数据交互
- 在ARM核上运行OpenCV做后处理
移植后的性能对比:
| 指标 | Artix-7 | Zynq-7020 |
|---|---|---|
| 最大频率 | 480MHz | 650MHz |
| 识别延迟 | 53ns | 68ns |
| 功耗 | 2.1W | 3.4W |
对于想进一步优化识别率的朋友,可以考虑:
- 在Zynq的PS端运行轻量级CNN(如SqueezeNet)
- 增加红外摄像头用于夜间识别
- 使用Sobel算子的方向信息辅助字符分割
这个项目所有源码和IP核都已开源,包括完整的Vivado工程文件和测试用例。在实际部署时,建议根据具体摄像头型号调整色彩转换矩阵,并针对当地车牌特点优化模板库。