十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA车牌识别加速:纯Verilog脉动卷积阵列实现毫秒级端到端延迟

FPGA车牌识别加速:纯Verilog脉动卷积阵列实现毫秒级端到端延迟 1. 车牌识别卡在延迟上先搞清楚延迟都去哪了做过车牌识别项目的人应该都有同感一到晚上、一到雨天、一到车快速通过闸机延迟和误检就开始冒头。刚开始我们用的也是市面上最常见的方案——摄像头采集网络传回上位机PC跑深度学习模型再把结果通过串口回传给闸机。这套方案在白天光线好的时候确实能用但端到端延迟动不动150毫秒以上闸机抬杆的时间足够后车司机按三下喇叭。真正让我下定决心把整条链路搬进FPGA的是一次雨天夜测车都过杆了电脑上的识别框才慢悠悠弹出来。后来我干脆把车牌检测和识别这套逻辑全部写进了FPGA里用纯Verilog手写了一个脉动卷积阵列加速器先后部署在Xilinx Artix-7和紫光同创Logos系列两块板卡上把端到端延迟从160毫秒压到了7毫秒以内。这篇文章不聊概念把我从架构选型、RTL设计、双平台移植到时序收敛的完整过程拆开讲给同样想做低延迟图像识别加速的朋友一个可参考的落地路径。先说一下整套系统的最终形态一个摄像头接进FPGAFPGA内部完成从图像采集、预处理、车牌检测、字符识别到结果输出的全部工作最终通过UART把识别到的车牌字符串发出去。全程不需要上位机参与属于真正意义上的单芯片端到端方案。文章后面涉及的关键点包括脉动卷积阵列为什么适合这种场景、纯Verilog手写RTL时怎么控制数据流和时序、Xilinx和紫光同创两套工具链之间到底有多少坑要填、以及最终延迟是怎么一步步压下来的。1.1 传统上位机方案的延迟到底花在哪先把账算清楚。一个典型的PC方案从摄像头到闸机动作延迟大约由五段组成延迟环节典型耗时说明摄像头采集与曝光5~10ms取决于快门速度夜间会更长网络编码与传输30~50msRTSP流、H.264编码、解包缓冲上位机解码与前处理3~5msOpenCV读帧、缩放、归一化神经网络推理20~60msCPU/GPU负载波动大GPU还有帧缓冲结果回传与执行器响应10~20ms串口/网络下发、PLC或单片机响应这里面最要命的是帧缓冲。摄像头送出来的数据要先凑成完整一帧、编码、推流、解码才能交给推理引擎。720p的图一帧在30fps下大约33毫秒一帧中途只要有一次网络抖动或者解码器卡顿延迟就会叠加上去。更是致命的在于这种延迟是不可预测的——你没法跟闸机的机械结构说这次快了下次慢点而车辆通行偏偏最怕不确定性。一串连续车辆通过时前车触发识别后车已经跟上系统一旦处理不过来就开始丢帧漏车。1.2 为什么选用FPGA而不是更强的CPU或GPU可能有人会问Jetson Orin不香吗香但场景不匹配。车牌识别这种固定摄像头场景有几个特点目标小、位置相对固定、语义简单、每帧最多一两辆车。这种负载用一个轻量CNN就够犯不上上几百TOPS的大算力平台。真正的问题在于延迟架构——GPU和CPU方案天然要等整帧然后批量推理整个数据流是断续的。FPGA则完全不同。它能把图像采集和计算真正做到流水线级并行像素从传感器出来经过灰度化、缩放、卷积、识别每一步都在一个时钟周期接一个时钟周期地推进。你不用等一帧图像完全采集完才开始处理而是来了几行就处理几行计算过程和采集过程完全重叠。这就是FPGA在超低延迟场景下不可替代的原因——不是算力强而是数据流动的节奏完全可控延迟是可以被计算和保证的。再加上车牌识别系统经常部署在户外停车场、高速收费站这种环境4W的板卡功耗和200W的GPU工控机相比部署成本和散热成本都差了一个量级。1.3 脉动卷积阵列在这套系统里扮演的角色我的目标不是只做一个能跑的FPGA车牌识别Demo而是做一个架构上可复用的CNN加速器。说直白点我今天想识别车牌明天可能想识别行人或者车型如果每次都要重新写一套卷积逻辑那项目没法迭代。所以我把核心计算单元做成一个通用的脉动卷积阵列上层通过配置来控制它跑哪一层卷积、用什么权重、输出到哪个模块。脉动阵列用纯Verilog实现不依赖任何第三方IP这意味着它在Xilinx和紫光同创之间可以无缝切换。2. 脉动卷积阵列的RTL设计数据流架构是性能的命根子脉动阵列这个词听起来高深但本质上就是一组规则排列的处理单元PE每个PE只和身边的邻居通信数据像流水线一样在PE之间流动。你可以把它想象成一条流水线上的工人每个人只干一件事乘加运算把半成品传给下一个整个阵列的吞吐量等于PE数量乘上时钟频率而不用像传统方案那样反复读写一大块数据总线。2.1 从卷积到矩阵乘的映射为什么脉动阵列天生适合CNN卷积运算本质上是一个滑窗乘加过程。3x3的卷积核在输入特征图上滑动每个位置要做9次乘法和8次加法。直接实现的话控制逻辑复杂而且乘加器利用率低——因为相邻位置的计算有大量重复读取。脉动阵列的做法是把卷积转换成矩阵乘的形式把输入特征图按滑动窗口展开成矩阵把卷积核权重排列成另一矩阵然后通过数据在阵列中的流动来完成矩阵乘法。这里有个关键点脉动阵列有三种经典数据流方式——权值固定Weight Stationary、输出固定Output Stationary、行固定Row Stationary。我最终选择的是Weight Stationary也就是权重预先加载到PE里输入激活从左边流进来部分和从上往下累加。选择这个方案有三个理由第一车牌识别用的CNN层数不多权重总量小完全可以一次性全部放上片上BRAM权值固定可以最大化复用。第二Weight Stationary的控制逻辑最简单状态机清晰纯Verilog实现时不容易出Bug。第三卷积核越小我们主要用3x3和1x1权重量越少片上存储压力越低整体设计越稳。下面是我单个PE的Verilog实现麻雀虽小五脏俱全module pe #( parameter DATA_W 8, parameter ACC_W 32 )( input wire clk, input wire rst_n, input wire en, input wire [DATA_W-1:0] act_in, // 来自左侧PE的输入激活 input wire [DATA_W-1:0] wgt_in, // 来自顶部PE的权重 input wire [ACC_W-1:0] psum_in, // 来自上方PE的部分和 output reg [DATA_W-1:0] act_out, // 激活向右传递 output reg [DATA_W-1:0] wgt_out, // 权重向下传递 output reg [ACC_W-1:0] psum_out // 部分和向下累加 ); reg [ACC_W-1:0] psum_r; wire [DATA_WDATA_W-1:0] mul_result; assign mul_result act_in * wgt_in; always (posedge clk or negedge rst_n) begin if (!rst_n) begin psum_r 32d0; act_out 8d0; wgt_out 8d0; end else if (en) begin psum_r psum_in mul_result; act_out act_in; wgt_out wgt_in; end end assign psum_out psum_r; endmodule这段代码里有两个细节值得说一下。第一个是en使能信号它不仅仅是控制PE工作的开关更重要的是控制数据流动的节奏。脉动阵列有一个概念叫气泡周期Bubble Cycle在卷积核切换行或者权重重新加载的时候阵列需要暂停几个周期让数据冲刷干净否则旧的激活还会残留在流水线里污染下一批计算结果。这个使能信号就是用来干这个的。第二个是输出寄存器每个PE的输出都打了一拍寄存器这样做表面上看增加了延迟但实际是必要的——它断开了组合逻辑的传播链是后面时序收敛能跑到150MHz的基础。没有这些寄存器64个PE串起来组合逻辑路径会爆炸。2.2 阵列规模与量化位宽怎么定脉动阵列做多大不是拍脑袋决定的要看你的CNN网络每一层的计算需求量。我设计的加速器目标网络只有9层卷积最大通道数64输入图416x416针对的是一个轻量检测网络。计算每一层的数据量后发现8x8共64个PE的阵列在150MHz时钟下单帧延时能做到3毫秒左右再加大阵列对帧延迟改善有限资源却成倍上涨。量化方案我选了8bit激活、8bit权重、32bit累加。这个配置在Xilinx的DSP48E1上刚好吃得下——一个DSP48E1完成一次8x8乘法累加路径走32bit进位链。8bit对车牌这种高对比度目标足够字符边缘清晰灰度值动态范围大量化误差基本不影响最终分类结果。但要注意的是中间累加一定要留够不能图省事用16bit夜间图像经过增强后灰度值偏移明显累加器很容易溢出。2.3 卷积层状态机层间切换和双缓冲脉动阵列的PE部分其实只占总工作量的一半剩下的一半是控制逻辑。每个卷积层的参数不同输入通道数、卷积核大小、步长、输出尺寸。这些参数在层与层之间切换时要重新加载权重、重置地址发生器、调整输入数据的流向。我写了一个layer_ctrl模块把这些参数存成一组寄存器状态机按帧推进typedef enum logic [2:0] { IDLE, LOAD_W, COMPUTE, DRAIN } layer_state_t; layer_state_t state, next_state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin layer_id 3d0; act_ram_addr 14d0; wgt_ram_addr 13d0; comp_en 1b0; end else begin case (state) IDLE: begin // 判断当前层的参数加载权重地址 layer_id layer_id 1; comp_en 1b0; end LOAD_W: begin // 双缓冲切换加载下一层权重到shadow buffer wgt_ram_addr wgt_ram_addr 1; if(wgt_ram_addr cur_layer_wgt_size) begin comp_en 1b1; end end COMPUTE: begin // 时序逻辑控制激活取数和列并行输出 end DRAIN: begin // 冲刷流水线输出最后几列结果 end endcase end end这里最重要的机制是双缓冲。权重存储用两块BRAM一块给当前层计算用另一块在计算的同时预加载下一层的权重。层间切换时不需要额外等权重加载时间状态机从COMPUTE跳到下一个LOAD_W直接无缝衔接。实际工程中这块逻辑是调试时间最长的部分——状态机条件判断里漏一个周期整个阵列的数据流就错位了而且这种错位只有在特定尺寸的特征图上才会触发仿真都很难抓。2.4 跨平台可移植的RTL编码规范既然目标平台有两个我在写RTL的第一天就定了几条死规矩。第一不使用任何厂商原语除了必须的PLL和DDR控制器——这两者各平台独立封装一层其余所有模块纯手写Verilog。第二内存全部用简单的单口/双口RAM端口模型自己包一层不调blk_mem_gen、不调紫光的RAM IP。第三复位全部用异步复位同步释放全局唯一复位域。第四时序约束涉及的时钟命名用统一前缀避免XDC和SDC语法差异。最后这条在配置PLL时特别明显。Xilinx的XDC里时钟约束用create_clock紫光PDS则完全兼容业界标准的SDC虽然也是create_clock但对时钟源引用的命名规则不同。我的做法是写两个版本的约束模板文件RTL里时钟端口命名保持一致顶层模块在实例化PLL时用generate块做平台条件选择。3. 从摄像头到车牌字符串FPGA内部流水线的划分很多人做FPGA图像处理时最容易犯的错误是把PC软件那套流程搬过来——采集完一整帧、存画中画、再做处理。在FPGA上这么做纯粹是浪费并行优势。正确的做法是让数据以流的方式穿过各个处理级行与行之间重叠处理。3.1 图像采集与预处理DVP接口、灰度化和缩放我的摄像头选的是OV5640通过DVP并行接口接入FPGA。720p分辨率帧率限制在30fps。DVP接口本身不复杂无非是用PCLK采样DATA加上HSYNC和VSYNC信号进行行列同步。但新手容易在行缓冲上栽跟头——卷积需要一个3x3窗口意味着你至少得缓存两行完整数据后才能开始输出第一行卷积结果。我用FPGA内部的BRAM搭了一个三行缓冲器每来一个像素输出一个3x3窗口数据给卷积阵列。预处理部分包含两个环节RGB888转灰度和双线性缩放。灰度化系数用的是标准的Y0.299R0.587G0.114BFPGA实现时用移位加法的并行结构替代乘法器节省DSP资源。这里我有一个经验可以分享夜间车牌识别容易翻车的根源往往不在算法精度而在图像增强不到位。我在灰度化之后加了一个自适应的直方图均衡模块对低灰度区域的对比度做拉伸实测夜间检测率从82%提升到了96%以上而且这个模块在脉动阵列之外独立运行不占一点点卷积资源。双线性缩放是另外一个容易忽略性能的地方。检测网络输入是416x416而摄像头输出是720p缩放不可避免地要引入插值。直接做双线性插值需要每个输出像素读四个输入像素但因为在流流水线中我们可以用行缓冲做插值窗口的缓存实际开销很小。3.2 检测网络固定摄像头场景下的轻量化CNN在整个RAL设计里检测网络选型是让我最纠结的部分。YOLOv4-tiny精度还行但网络偏深需要大量FPGA逻辑去调度层间切换。最终选择了一个更精简的结构——以TinyYOLO为骨架但砍掉FPN层因为固定摄像头视野下车牌尺寸变化有限不需要多尺度特征融合。层号类型卷积核通道步长输出尺寸L1Conv3x3161416x416L2Conv3x3162208x208L3Conv3x3321208x208L4Conv3x3322104x104L5Conv1x1641104x104L6Conv3x364252x52L7Conv1x132152x52L8Conv3x332152x52L9Conv3x316152x52每层卷积后都有BatchNorm和ReLU我在FPGA里把BN层融合进了卷积做法是在加载权重时对权重做预缩放省掉了推理时一个完整层。这是一个非常关键的性能优化——BN不融合的话每次卷积后都得单独做一个除法在FPGA上除法器代价很高。由于摄像头是固定的我还在检测前加了一个帧间差分模块做运动区域预定位。没有车辆经过时检测网络不工作只有画面中出现运动目标才触发卷积计算。这不仅省电还能减少误检——路边静止的广告牌就算长得再像车牌也不会被识别。3.3 车牌字符识别把LPRNet压缩到脉动阵列上车牌字符识别的路线有两种先分割字符再逐个识别或者用类似LPRNet的序列识别方法免分割。我选择LPRNet方案因为它在工程上更适合FPGA——不需要字符分割算法里的连通域分析而连通域分析恰恰是FPGA上最不擅长的复杂控制逻辑。LPRNet的输入是检测网络裁剪出的车牌图统一缩放到96x32经过若干卷积层提取特征后输出一个序列特征图最后通过全连接层映射成字符串概率。7位车牌包含一个汉字、一个字母和五个数字字母输出类别是31个汉字24个字母10个数字共65类。在脉动阵列上跑LPRNet没有想象中复杂。权重总数加在一起大约200KB双缓冲BRAM刚好放得下。卷积层的调度逻辑和检测网络共用同一套layer_ctrl状态机只要重置层参数表就行。真正费工夫的是最后的全连接层——脉动阵列做矩阵乘没问题但全连接的输入是一个拉平的长向量需要把数据从特征图格式转换成向量格式我在DMA读取控制里加了一个地址映射表来解决。3.4 后处理置信度过滤、跟踪稳定与结果输出后处理部分我坚定地采用硬件逻辑不折腾软核的路线。检测网络输出52x52的特征图每个点有4个边框参数和2个类别概率。因为固定场景下每帧最多一两辆车NMS的候选框数量很少我可以直接用硬件做一个简单的排序选择器把置信度最高且交并比大于阈值的目标保留下来。跟踪稳定逻辑是后来加上的功能。最初版本单帧识别偶尔会跳字比如京A12345变成京A12344排查后发现是曝光抖动导致字符特征扰动。我在FPGA里做了一个三帧投票模块连续识别同一个字符串三次才输出否则沿用上一帧的结果。这个简单的机制把识别稳定性从94%拉到了99%以上代价仅仅是多消耗了几百个LUT。最终结果通过UART发送波特率115200数据格式就是一串纯文本字符加回车换行。这个设计让FPGA可以无缝对接停车场的各种PLC或者闸机控制器也方便调试时用一根USB转TTL线直接插电脑看结果。4. 双平台部署实录Xilinx与紫光同创的移植避坑标题里既然写了Xilinx和紫光同创双平台部署就得把这部分经验讲透。我最初在Xilinx Artix-7上完成验证然后把整个工程移植到紫光同创Logos系列PGL50H上。两者的RTL代码共用率达到95%以上但剩下5%的工程细节折腾了我整整一周。4.1 Vivado与PDS两套工具链的最典型差异Xilinx用Vivado紫光同创用PDSPango Design Suite。PDS的综合引擎用的是Synopsys的Synplify Pro定制版综合策略和Vivado的工程化思维差别很大。最大的感受是PDS对SystemVerilog的支持不完整我代码里原本用了一些SV的接口语法和typedef enum结构在Vivado里编译得好好的拿到PDS里直接报语法错误。这给了我一个深刻的教训真要在国产FPGA上做项目就老老实实写标准Verilog-2001别用SV的花哨语法。后来我把代码里的SV特性全改成Verilog写法两个平台才都能顺利综合。另一个明显的差异是综合策略。Vivado在资源利用率吃紧时会自己推断优化方式而PDS更倾向于遵守用户给的(* parallel_case *)、(* full_case *)这类综合属性。如果编码时不留心同样一段逻辑在两种工具下综合出的资源差异可能达到20%。我的经验是每次都写完备的case不要有默认分支依赖否则两个平台的行为会不一致。4.2 原语与IP跨厂商可移植的关键隔离层RTL代码里最容易绑死平台的是两个地方PLL时钟管理和DDR控制器。PLL这块无法绕过厂商差异但我把它的调用严格限制在一个clk_gen模块里顶层只引出一个200MHz主时钟和100MHz低速外设时钟其余模块根本不知道时钟是怎么生成的。同理我把自己写的通用双端口RAM封装了一个统一接口内部根据平台选择直接推断BRAM或调用厂商内存原语。具体踩过一个坑Xilinx BRAM的写使能是字节使能wea按字节位宽控制紫光的PDS内存模块默认给出的写使能时钟也可能有相位差。我用一个generate块把两套实现隔离Xilinx用标准Verilog推断BRAM紫光PDS下调用厂商原语两个平台各留一套接口。写使能时序的差异在我测试Xilinx板卡完全正常换到紫光板卡后出现了偶发数据错误用片上逻辑分析仪抓了半天波形发现是BRAM写地址在使能信号拉高半个时钟周期后才稳定。最终在RAM封装模块里对写使能又打了一拍寄存器才解决。4.3 时序约束迁移从XDC到SDC要注意的语法差异这块是文档最少、查资料最头疼的部分。Vivado的约束文件是XDCPDS用的是SDC。虽然SDC是业界通用格式但PDS在语法解析上有自己的脾气。最典型的是对时序例外命令的处理优先级同样一条set_false_pathVivado默认不区分先后顺序只要约束合法就全部生效PDS却严格按命令的先后顺序处理靠后的命令会覆盖靠前的。这导致我从Vivado导入SDC时有一条跨时钟域的异步FIFO约束被后面的多周期路径约束覆盖了结果PDS布局布线报时序违规差点被误导去优化根本没有问题的计算逻辑。解决办法是重新梳理每个时序约束先定义所有时钟再写set_clock_groups -asynchronous明确隔离跨时钟域最后才列set_max_delay之类的路径例外。顺序清晰后PDS的结果和Vivado完全一致。4.4 两台板卡的资源占用与功耗对比同样一套RTL两个平台最终吃掉的资源差别让我挺意外资源类型Xilinx Artix-7 xc7a100t紫光同创 PGL50HLUT41%53%触发器36%44%DSP48%46%BRAM(36Kb)61%66%最大时序频率152MHz138MHz整板功耗3.8W4.3W紫光PGL50H的LUT利用率比Xilinx高不少主要原因不是架构差而是PDS的综合器对移位寄存器和多路选择器的映射效率偏低同样的地址发生器逻辑在Vivado里会被综合成BRAM输出数据选择结构在PDS里则变成了大面积的LUT阵列。另外PDS对部分时钟域的布局约束不如Vivado智能导致时序频率低了约10%。但要注意PGL50H在国产FPGA里定位本来就是中低端如果上游供应链对国产器件有硬性要求这个性能差距换来的自主可控是值得的。5. 超低延迟的实测数据与收尾我搭建了一套完整的测试环境来验证延迟指标一个型号固定的摄像头对准测试车位FPGA板卡通过USB转串口连上位机用示波器同时测三个信号点——摄像头帧有效信号、FPGA识别结果有效信号、闸机继电器动作信号。测的是从帧数据进入FPGA开始到UART输出车牌号为止的时间。5.1 延迟预算拆解最终测出来的数据在预期之内但有一个环节比预想中耗时处理级耗费时间说明三行缓冲与灰度化0.16ms每行像素进入即开始处理直方图均衡增强0.3ms需要统计整帧灰度直方图延迟在两个视频行之间双线性缩放0.5ms416x416输出逐行处理流水线重叠检测网络卷积计算3.2ms9层卷积150MHz8x8脉动阵列NMS候选框筛选低于0.05ms电路全并行最多4个候选框裁切与缩放0.3ms针对车牌区域单独的数据通路LPRNet识别1.5ms12个卷积层全连接串行执行三帧稳定与UART输出忽略三轮投票逻辑不增加额外帧延迟从帧有效到UART送出第一个字符实测5.8毫秒。再算上UART发送整个字符串的耗时约0.7毫秒端到端约6.5毫秒。对比之前上位机方案的160毫秒加速比接近25倍。更重要的是这个延迟是确定性延迟——不管系统跑多久不管后端负载怎么变化5.8毫秒就是5.8毫秒这对闸机联控场景来说意义远大于单纯的快。5.2 时序收敛的几个关键动作整个RTL工程能跑到152MHzXilinx不是靠运气是靠三次关键优化。第一次优化在PE阵列内部。最初每个PE的累加器用了纯组合逻辑加法array外部直接取部分和综合后发现64个PE连成的加法链成了关键路径频率只有90MHz左右。解决办法是将每个PE内部的乘法结果再打一拍累加路径在DSP48E1后级寄存器里完成频率提升到130MHz。第二次优化在卷积控制状态机。原始的layer_ctrl用了大量组合逻辑做地址生成导致从状态机到BRAM地址端口的路径过长。我把地址生成逻辑改成了简单的计数器利用BRAM输出寄存器的特点把地址建立时间约束拉松。第三次优化是让三行缓冲的输出窗口直接接进激活输入端口。原本在缓冲器后有简单的灰度选择逻辑但预设条件较多延时大我把它做成独立的节点让像素在缓冲器内部就完成了灰度判定。5.3 纯Verilog实现这个项目的几个体会最后说点个人经验。用纯Verilog手写脉动卷积阵列工作量确实比用HLS大得多但这个工作量换来的是完全可控的时序和极好的跨平台可移植性。HLS生成的RTL在两个FPGA平台之间几乎没法直接迁移因为HLS生成的微架构往往深度绑定厂商IP——你要是用Vivado HLS写基本就只能留在Xilinx平台。而纯Verilog配合IP隔离层反过来让我在两块板卡之间来回切换甚至不用改一行计算核心代码。紫光同创的PDS工具链相对年轻文档和社区资料远不如Xilinx丰富很多问题需要自己到官方论坛提问或者对照波形排查。如果你的项目有明确的国产化要求这关迟早得过。我的建议是RTL代码从第一天起就要把厂商相关的东西全部封装隔离宁可多写一点胶水逻辑也不要在核心代码里碰厂商原语。另外车牌识别这个场景给了脉动阵列一个很合适的施展空间——网络规模小、时序可预测、延迟要求极端。这次做完之后我打算把同一套加速器结构用于车型分类和车辆颜色识别在不改变硬件架构的前提下大概率只需要替换网络配置表和权重文件。脉动卷积阵列这种架构的投资回报率在这个方向上算是拉满了。
返回列表