news 2026/9/8 15:36:20

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA车牌识别加速:纯Verilog脉动阵列实现毫秒级延迟

做车牌检测识别,最怕的不是算法不够花哨,而是延迟压不住。停车场闸机前那几秒,车停稳、摄像头抓拍、识别、抬杆,整个过程但凡多犹豫一下,后面就是一串喇叭声。这个项目里我用纯Verilog搭了一套脉动卷积阵列加速器,没有调现成的IP核,也没用HLS,硬是在Xilinx和紫光同创两家的FPGA上把车牌检测和识别的完整流程跑通了,端到端延迟做到几十毫秒级别。整套东西的思路、RTL实现细节、跨平台移植的坑和性能优化过程,我在这篇文章里完整摊开,给准备在FPGA上做AI加速的同行当个参考。

1. 车牌识别场景的算力账单:为什么非得用专用加速器

1.1 从抓拍到抬杆:延迟预算怎么分配

停车场入口的完整动作链大概是这样的:车辆驶入、地感线圈触发、摄像头抓拍、图像传给处理单元、检测车牌位置、识别车牌字符、查库比对、抬杆放行。传统方案里,图像处理一般放在工控机或者服务器上,摄像头到主机之间哪怕走的是千兆网,一帧720p图像也有几毫秒的传输和拷贝开销。要是再赶上主机负载高、进程调度抖动,识别延迟从100ms飙到几百毫秒很正常。

FPGA的优势在于数据不需要上CPU、不需要经过DDR反复搬运,摄像头进来的数据直接在片上流转。但FPGA也有自己的问题:它本质上是一个"搭积木"的硬件平台,用错了架构,资源利用率低、时序收敛困难,最后可能还不如一颗跑着OpenCV的ARM芯片。

所以这个项目的第一个设计决策很明确:把整个识别流程全部放在FPGA内部,不借助额外处理器。我的延迟预算是这样拆的:图像采集和预处理5ms以内,检测网络30ms以内,车牌区域裁剪和识别网络10ms以内,结果输出1ms以内,总共控制在50ms左右。这个预算对标的是"司机基本感觉不到等待"的体验,同时给系统的后续扩展留出余地。

1.2 通用处理器为什么跑不动:两个硬瓶颈

很多人一开始会问:车牌识别而已,用ARM加OpenCV不是也能做吗?能,但要看计算量。车牌检测如果用一个轻量级卷积网络,输入320x240灰度图,总共大概1亿多次乘加运算。听起来不多,但在一颗300MHz的ARM处理器上,即使编译器做了很好的优化,每个乘加平均也要几个甚至十几个周期,再加上内存访问、循环控制的开销,跑完整个网络得上百毫秒。

这里面的第一个瓶颈是"冯诺依曼墙":数据和指令都要从内存取,卷积运算的访存密度又特别高,缓存命中率稍有波动性能就掉一截。第二个瓶颈是"并行度不足":ARM核心再多也就四个八个,而卷积运算本身是高度并行的——不同输出通道之间、不同空间位置之间互不依赖,这种并行度需要大量计算单元同时发力才能榨干。

FPGA恰好在这两点上都是强项:数据在片上流动,不走通用内存总线;逻辑阵列可以同时铺开几十上百个乘加单元。但FPGA也不是随手写写就能发挥威力的,真正把性能做上去,需要一个规整的、数据复用率高的计算架构。脉动卷积阵列就是为这种场景准备的。

1.3 脉动卷积阵列的直觉理解

脉动阵列这个概念听起来高深,其实拿流水线来类比就很好懂。想象一条流水线,每个工位只干一件事——把从上游传来的半成品加上自己手里的一份材料,再传给下游。工位之间不需要来回取料,每个人都专注在自己的岗位上,原料从头流到尾,成品就从另一端源源不断出来。

对应到卷积计算:每个处理单元就是一个"工位",它内部寄存着一个权值,输入数据从阵列的一端流入,经过每个PE时做一次乘加运算,部分积累加结果在阵列中流动。整个过程数据局部性极好,每个输入像素会被多个滤波器反复使用,但这种反复使用全部发生在片内,不需要反复从内存读。同时脉动阵列结构非常规整,全部由重复的PE单元组成,非常适合用Verilog这种RTL语言描述,逻辑清晰、便于规模扩展。

2. 整体架构:检测识别两级流水,共用一块加速阵列

2.1 一个阵列跑两个网络:复用而不是堆硬件

车牌识别全流程实际包含两个神经网络:前面是目标检测网络,负责从整幅图像里找到车牌的位置框;后面是识别网络,负责把裁剪出来的车牌图像映射成字符序列。如果给每个网络各配一套加速器,资源翻倍不说,逻辑复杂度也上来了。

我的做法是设计一个统一的脉动阵列计算核心,通过一个指令调度器在运行时切换网络参数。检测网络跑完,得到车牌框坐标后,同一块阵列再加载识别网络的权重,继续跑识别推理。这么做的好处是FPGA资源利用率高,在Xilinx和紫光同创的中等规模器件上都能放下,同时调度逻辑也不复杂——本质就是一个状态机,当前网络执行完毕后,切换权重加载指针和输入数据源。

整个系统数据流是这样的:摄像头并行输入先经过灰度化和缩放预处理,写入双帧缓冲;检测阶段从帧缓冲A读图像,输出车牌框坐标;根据坐标从帧缓冲B中截取车牌区域,作为识别网络的输入;识别结果经过解码后通过UART或以太网输出。帧缓冲设计成双缓冲是为了实现乒乓操作——摄像头正在写入bank A的时候,加速器可以读取bank B的数据,互不干扰。

2.2 数据通路设计:绕开DDR的片上方案

设计初期我认真考虑过要不要加一颗DDR3做帧缓存,毕竟很多FPGA图像处理方案都离不开DDR。但仔细一算发现,车牌识别的输入分辨率根本不需要做到1080p,640x480的灰度图足够用了,一帧才307KB。而常见FPGA的片上BRAM资源有几百KB到几MB,完全能够容纳两到三帧灰度图。

于是整个数据通路完全绕开DDR,图像数据从摄像头进来后直接写入片上BRAM。这个决策对延迟的贡献极大:省去了DDR读写带宽竞争,也省去了DDR控制器的复杂逻辑,更重要的是端到端延迟变得极其可控——每个模块之间的握手信号是确定的,不会出现DDR仲裁导致的随机延迟。

行缓冲(Line Buffer)也是数据通路里的关键模块。卷积运算需要用到一个3x3的滑动窗口,而图像数据是按行连续输入的。行缓冲的作用就是缓存两行完整数据,配合当前行数据,拼出三行数据后再取3x3窗口。这个模块用移位寄存器或者小容量RAM实现,逻辑不复杂,但很容易写错,后面我会专门讲踩坑经验。

2.3 量化方案:INT8够不够用

神经网络权重默认是FP32浮点格式,但FPGA做浮点运算资源开销巨大。车牌识别这个场景对量化非常友好,因为车牌字符的边缘、纹理特征是强结构信息,不像自然图像那样对噪声和高频细节那么敏感。

我采用的是常见的INT8对称量化方案:先在校准集上统计每一层激活值的绝对值最大值,算出缩放系数scale,再把浮点权重和激活值映射到[-127, 127]区间。卷积计算过程中,乘加累加器保持32位精度,避免中间结果溢出,每层输出经过ReLU和量化后再回到8位。这样做的精度损失在车牌数据集上实测不到0.5%,完全在可用范围内。

这里有一个容易被忽略的细节:累加器尽管是32位,卷积层的累加次数是有限的。如果中间结果超过INT32范围,就需要提前截断或者重新安排量化策略。我用了一个宏定义来配置累加器位宽,初期设成40位多留裕量,综合发现问题后再裁剪,确保不会出现溢出导致精度崩掉的情况。

3. 纯Verilog实现脉动阵列:核心模块逐个拆

3.1 PE处理单元怎么写:一段可以被综合器吃透的RTL

脉动阵列的基本单元就是PE,麻雀虽小五脏俱全。单个PE内部需要做三件事:锁存自己的权值、接收输入数据并传下去、将输入与权值相乘并累加部分和。这里我贴一段核心代码,这是整个项目里所有PE的骨架:

module pe #( parameter DW = 8, // 数据位宽 parameter AW = 32 // 累加器位宽 )( input logic clk, input logic rst_n, input logic w_load, // 权值加载使能 input logic [DW-1:0] w_in, // 从上游输入的权值 input logic [DW-1:0] act_in, // 从上游输入的激活值 output logic [DW-1:0] act_out, input logic [AW-1:0] psum_in, // 上游部分和 output logic [AW-1:0] psum_out ); logic [DW-1:0] w_reg; logic [AW-1:0] psum_reg; always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin w_reg <= '0; psum_reg <= '0; end else begin if (w_load) w_reg <= w_in; act_out <= act_in; // 激活值打拍向下游传递 psum_reg <= psum_in + $signed(w_reg) * $signed(act_in); end end assign psum_out = psum_reg; endmodule

这段代码有几个关键点。第一,权值寄存器只有在w_load信号有效时才更新,计算阶段权值保持不动,这就是典型的"权值固定"数据流。第二,乘法结果直接加到部分和上,累加器宽度要比乘数宽度宽得多,防止溢出。第三,act_out在时钟沿后输出等于当前拍的act_in,相当于数据在阵列中每经过一个PE就延迟一个时钟周期,实现自然的数据流水传递。

我建议所有乘法都写成$signed()显式有符号乘法,避免综合器把INT8当作无符号数处理,否则负权重会算出完全错误的结果。

3.2 数据流控制:权重加载、输入流动、结果排空

搭建好单个PE后,难点转移到整个阵列的数据流控制。一个16x16的脉动阵列,在实际跑卷积时不能简单地把图像怼进去就完事,它需要三个明确的阶段:LOAD_WEIGHT、STREAM_IN和DRAIN。

LOAD_WEIGHT阶段把当前卷积层所有滤波器的权值卸载到PE阵列内部寄存器。这一阶段常被新手忽视,但权值加载是耗时大户。假设一个卷积层有64个输出通道、32个输入通道、3x3卷积核,总权重数就是64x32x9=18432个。如果每个周期加载一个PE,加载完这些权重需要18432个时钟周期。在150MHz时钟下就是123微秒,听着不长,但网络有几十层,累积起来就非常可观了。

STREAM_IN阶段才真正开始做卷积计算。输入特征图从行缓冲生成滑动窗口后,窗口数据按照脉动阵列的节奏送入第一行PE,部分和在阵列中向下累加。这个阶段必须严格保证数据到达时间和PE计算节拍一致,否则序列完全错位,输出结果就是错的。

DRAIN阶段则是把阵列里还没流出的部分和全部排空。我最初的设计没有显式处理排空,导致最后一行卷积结果永远算不完,后来在状态机里加了计数器,在输入结束后多等PE阵列深度个时钟周期,问题才解决。

3.3 配套模块:行缓冲、ReLU、池化和量化

脉动阵列只是"发动机",周边配套模块决定它能不能正常运转。行缓冲模块负责从图像帧缓存里读数据,拼出3x3窗口;ReLU激活在数字电路里其实就是判断符号位,负数清零,一个二选一多路选择器就搞定;最大池化做一个两两比较的树形结构;量化模块做饱和截断或者四舍五入。

这些模块单独拎出来都很简单,难的是它们的时序要和脉动阵列完全对齐。我是用计数器统一管理整个流水线的节拍:图像行计数器、窗口内偏移计数器、输入通道计数器、输出通道计数器,每个计数器的溢出条件都预先在仿真里验证过。如果你也打算自己写,强烈建议先把这些计数器的时序图画出来再写代码,不要直接上手Verilog,否则大概率会在仿真阶段被各种边界条件折磨。

4. Xilinx与紫光同创双平台落地:纯Verilog的隐藏优势

4.1 两个平台的差异:从EDA工具到底层原语

这个项目的目标平台从一开始就是两个:Xilinx的7系列/Zynq系列,以及紫光同创的FPGA。很多FPGA工程师会习惯性地大量使用厂商IP核,比如Xilinx的FIFO IP、Block Memory Generator、MMCM时钟IP。这种做法在单一平台开发时效率很高,但要做跨平台移植就非常痛苦——Vivado生成的IP核在紫光同创的Pango Design Suite(PDS)里完全无法使用,只能重新生成对应平台的IP,而两边的接口参数、复位策略、时钟约束都不完全兼容。

项目强调"纯Verilog"的价值就在这里:所有逻辑模块全部用行为级RTL编写,不依赖任何厂商特定的IP原语。FIFO自己写,RAM自己写,时钟模块封装成独立的wrapper,内部才根据平台例化不同的时钟原语。这样做的代价是前期的开发量更大,但换来的是代码在两个平台之间可以无缝综合,省去了移植时大量的返工。

实际操作中两边的差异主要体现在这几个方面:Xilinx用Vivado和XDC约束,紫光同创用PDS和PDC约束;Xilinx的DSP原语是DSP48E1/E2,紫光同创有自己的DSP单元结构;块RAM的容量和排列也不同,但这些都是物理资源层面的差异,逻辑代码本身不受影响。我只需要把"平台相关"的部分收敛到几个顶层模块里,其余80%的RTL代码两边完全一致。

4.2 移植实操与资源、时序评估

具体移植时,我的做法是建两个工程,共享同一套RTL源文件,只有平台适配层不同。平台适配层包含三部分:时钟生成、复位同步、调试接口。时钟生成在Xilinx下例化MMCM,在紫光同创下例化它自己的PLL,封装成统一的clk_gen接口;复位逻辑统一用同步复位加异步断言的方式,避免两家工具的复位树综合差异;调试接口在Xilinx上走ILA,在紫光同创上用PDS的逻辑分析仪。

资源占用方面,拿我最终使用的8x16=128个PE阵列举例,每个PE对应一个乘加单元,映射到两个平台都需要约128个DSP单元。Xilinx Zynq XC7Z020有220个DSP,资源占用58%;紫光同创同等规模逻辑的器件也基本够用。BRAM主要用于帧缓冲和权重存储,我分配了约1.5Mbit的BRAM资源。整体LUT占用在40%左右,时序在150MHz约束下两个平台都能收敛。

关于工作频率,我一开始在Xilinx上跑到200MHz很轻松,但同样的代码放到紫光同创器件上,200MHz就时序违例了。后来分析发现主要是因为两家的DSP和BRAM布局布线特性不同,最终把目标时钟统一调整到150MHz,两个平台都能稳定收敛。这种事只有双平台同时做才会碰到,如果只盯着一家器件,可能根本不会注意到综合器对代码结构的偏好差异。

5. 超低延迟优化实录:把延迟一毫秒一毫秒抠出来

5.1 端到端延迟分解:瓶颈往往不在乘法器

我花了很长时间优化延迟,最后发现一个反直觉的结论:脉动阵列本身的乘加计算只占端到端延迟的一小半,真正吃时间的反而是穿插在计算周围的各种"等待"。我把自己实测的一帧数据做了个延迟分解,大概是这样:

  • 图像采集和灰度/缩放预处理:5ms
  • 检测网络推理:约18ms
  • 车牌区域裁剪和预处理:3ms
  • 识别网络推理:约6ms
  • 结果编码和输出:1ms内

总耗时约32ms。其中的检测网络推理18ms包含了每层的权值加载、计算、排空、以及层间切换的流水线气泡。如果只算纯计算时间,128个PE在150MHz下的峰值算力是19.2GMAC/s,而我这个检测网络大约1.5亿次MAC,理论计算只要8ms左右。多出来的10ms基本都消耗在权值加载和流水线等待上。

这个差距说明了一个道理:对于中小规模的加速器来说,性能瓶颈不是算力不足,而是数据供给速率和调度效率。如果你也在调类似系统,建议先做延迟分解,别一上来就堆PE数量。

5.2 我踩过的三个性能坑

第一个坑是权值加载时间被严重低估。最初的实现里,每个卷积层之间串行执行:当前层计算完、把结果写回缓存、然后才开始加载下一层的权重。每一层平均增加120微秒左右的加载时间。解决办法是给权重存储做双缓冲:当前层正在计算时,下一层权重已经在另一个缓冲区里加载完毕,层切换时直接切换数据源,几乎零开销。这个优化让检测网络的推理时间从27ms降到了18ms。

第二个坑是批次归一化(BN)层在推理时的融合。最初网络里每一层卷积后面都跟了单独的BN计算模块,用BRAM存均值和方差参数,算完还要再做一次乘法缩放。后来我把BN参数融合到卷积层本身的权重和偏置里,在量化阶段就把融合做完,FPGA上完全不需要单独的BN模块,省掉的不仅是计算时间,还有BN参数占用的存储和搬运开销。

第三个坑是边界处理。车牌图像经过多次卷积后特征图尺寸会缩小,而卷积本身需要padding才能保证边缘像素参与计算。我之前图省事,直接在行缓冲里把边界值固定为0,结果输入图像边缘的车牌边框特征计算错误,检测率掉了一截。最后还是在输入特征图进阵列之前,由行缓冲控制器根据坐标判断是否处于边界,边界处补零、否则透传数据,问题才算解决。

6. 调试经验与常见问题速查

6.1 仿真验证三板斧:从单元到系统逐层推进

脉动阵列这类高度规则的结构,调试起来有一个好处:一旦PE本身验证正确,整个阵列的行为就基本可控。我的验证策略分三步走。第一步是单元验证,单独仿真PE模块,喂几组已知的权值和输入值,断言计算结果的正确性,这一步能同时验证有符号乘法和累加逻辑。第二步是小规模阵列验证,用4x4的阵列搭配已知权重,验证数据流方向和流水节拍是否正确。第三步才是整机验证,把阵列、行缓冲、量化模块、网络调度器全部连起来,用仿真图像跑完整网络,与软件模型逐层对比输出。

所有验证模块都用SystemVerilog断言和testbench完成,纯Verilog项目的好处是仿真不依赖任何厂商工具,ModelSim、Vivado Simulator、开源的iverilog都能跑,非常灵活。

6.2 上板调试:把"看不见"的状态拉出来

上板调试和仿真完全是两回事,很多在仿真里永远不会出现的问题,在上板后都会暴露出来。我遇到过的几个典型问题整理成了一张速查表,方便大家排查:

现象可能原因排查方法
输出全是0权值加载失败或使能信号没拉起来检查权值加载状态机,抓LOAD_WEIGHT阶段的完成信号
图像有重影或错位行缓冲写地址和读地址错拍打印行缓冲读写指针,和理想时间轴对比
检测框整体偏移卷积padding逻辑有误,边缘特征计算错误核对边界判断条件,仿真中强制检查边缘像素路径
偶发时序错误跨时钟域信号没做同步检查所有跨时钟域握手信号,补两级寄存器同步
精度和软件模型偏差大累加器溢出或量化scale不准逐层对比中间特征图,定位最大偏差层

上板调试时逻辑分析仪是必备工具。我的经验是把"层完成标志"、"帧结束标志"、"量化输出有效"这几个关键信号全部拉出来,用状态机配合计数器定位问题出现的具体帧和具体层,这样可以把随机性问题缩小到具体环节,效率远高于瞎猜。

6.3 一个关于跨平台的独门建议

最后分享一个我觉得特别重要的经验:跨平台项目千万别等一个平台全部调通了再移植到另一个平台。我的做法是代码开发阶段就保持两个平台的综合脚本同步更新,每次大改动后,两个平台各跑一次综合和时序分析。这样做的好处是能尽早发现"只在某个平台能综合"的代码风格问题。比如某些for循环在Vivado里能正确展开,在PDS里可能因为综合策略不同导致逻辑膨胀,这类问题越早发现,返工成本越低。

写在最后的体会

这套系统做完之后,我最大的感受反而是轻舟已过万重山。脉动阵列、纯Verilog、双平台部署,每一项单独拿出来都不算新鲜,但把它们组合在一起解决一个具体的实时场景,中间还是有不少朴素的工程智慧。如果回到项目初期,我会更早地做延迟分解,更早地做权重双缓冲,更早地正视两个平台之间的工具链差异。每个环节单独看都是小问题,攒在一起就可能让整个项目延期。这套方案后续还可以扩展的方向很多,比如把阵列规模加大、加入多核并行、扩展到其他实时检测任务。但就车牌检测识别这个场景来说,一块中等规模的FPGA、一套纯RTL实现的脉动阵列,已经能交出足够让人满意的"超低延迟"答卷了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 15:33:34

AI搜索重塑用户决策链路,企业内容生态迎来规则重写

当用户从“搜网页”转向“问AI”&#xff0c;信息获取的底层逻辑正在发生根本性位移。艾瑞咨询的调研显示&#xff0c;超过四成网民已习惯通过生成式AI获取知识或解决方案&#xff0c;这一比例在25至35岁的商业决策群体中更高。用户不再逐一翻看搜索结果页&#xff0c;而是期待…

作者头像 李华
网站建设 2026/9/8 15:32:44

ARM体系与Cortex系列全面解析:从架构基础到嵌入式实战避坑

1. 先搞清&#xff1a;ARM 究竟在说什么——架构、微架构与产品命名 很多刚接触嵌入式的朋友&#xff0c;一上来就被“ARM 体系”“Cortex”“内核”这些词给绕晕了&#xff0c;三个词经常混着用。我做了这么多年嵌入式&#xff0c;踩了不少坑之后才把这条线理顺&#xff1a; …

作者头像 李华
网站建设 2026/9/8 15:30:41

2026北京公司注册流程深度解析:五家代办机构资质对比与创业推荐

北京创业环境与注册代办市场现状北京创业氛围持续升温&#xff0c;新设企业数量保持较快增长。统计数据显示&#xff0c;2025年全市新设经营主体38.01万户&#xff0c;同比增长20.7%&#xff0c;其中新设企业32.1万户&#xff0c;平均每天新设超过1000户&#xff0c;创业环境持…

作者头像 李华
网站建设 2026/9/8 15:28:08

WPF DataGrid动态行显示:基于RowStyle与DataTrigger的MVVM实现

最近在做 WPF 项目时碰到一个挺典型的诉求&#xff1a; DataGrid 要按条件动态控制某些行的显示与隐藏 。乍一听好像不难&#xff0c;但真上手你会发现&#xff0c;直接操作 Row.Visibility 会踩坑&#xff0c;绑定集合又容易把逻辑写散。我把自己实际调试通过的一套方案整…

作者头像 李华
网站建设 2026/9/8 15:26:42

macOS版Codex智能体编程工具实战指南:安装配置与使用技巧

macOS 开发者们&#xff0c;最近圈子里讨论热度最高的消息&#xff0c;应该就是 OpenAI 正式发布 macOS 版 Codex 应用这件事了。如果你还没试过&#xff0c;我强烈建议你看完这篇再决定要不要装。Codex 并不是简单的"把 ChatGPT 塞进终端"&#xff0c;它背后是一套独…

作者头像 李华