纯Verilog实现FPGA脉动阵列车牌识别加速器:从架构到部署
做这个项目的起因很直接手头有车牌识别的需求但通用方案在端到端延迟上始终压不下去。软件推理哪怕做了轻量化视频帧进来之后也要先解码、缩放、再跑网络一帧下来几十毫秒是常事而传统的FPGA图像处理又大多停留在边缘检测、二值化这类单点加速上识别部分还是交给上位机。于是我把整个车牌检测与识别流程全部搬进了FPGA用纯Verilog手写了一个脉动卷积阵列加速器算力核心用经典Systolic Array架构识别部分也从二值化、边缘检测、字符分割一直到轻量CNN全都在硬件里跑完。整套设计先后在Xilinx的Artix-7和紫光同创的PGL22G两片FPGA上部署验证过视频帧进来到车牌字符串从UART输出的端到端延迟实测可以压到毫秒级以内100MHz逻辑频率下约2~3ms量级。这篇文章会把架构怎么拆、参数怎么定、两个平台部署时分别踩了哪些坑全部整理出来适合那些已经在学FPGA、想上手一个完整图像处理项目的朋友。1. 整体设计与思路拆解1.1 为什么是脉动卷积阵列而不是DSP串行算乘加先把脉动阵列这个东西说清楚。很多人第一次听到Systolic Array是在看TPU相关文章的时候Google架构里的核心计算单元就是它。它的本质是一组排列成矩阵的PEProcessing Element每个PE只和相邻PE通信数据沿着固定方向、像心脏脉搏一样有节奏地“泵”过整个阵列。这种结构最大的好处是数据复用率极高一个特征图数据加载进阵列之后会在多个PE之间流动每个PE各取所需不会出现传统DSP乘法器那样每个乘加操作都要把操作数从BRAM里拉一遍的带宽压力。车牌识别这个任务计算量最大的部分是卷积和全连接。YOLO那种大网络在纯RTL里实现意义不大因为FPGA资源有限我更倾向于把车牌定位和字符识别做成一个“轻量CNN传统视觉”的混合流程。然而即使是轻量CNN比如3x3卷积、通道数十几层的结构如果在FPGA里老老实实用单个DSP做乘累加逐个滑窗计算一个特征图上万次乘加跑下来延迟轻松突破10ms。脉动阵列天然擅长这种规律性强、大量重复乘加的计算正是因为数据流动模式可以在硬件上完全流水化每个时钟周期阵列内部所有PE同时工作8x8阵列单周期就是64次乘加等效算力比单DSP高出一个量级。这个选择还有一个隐蔽的好处脉动阵列的数据流是完全规则的。权重提前驻留在PE寄存器里输入特征图按周期流入部分和在垂直方向累加写入输出寄存器。这种结构非常适合用Verilog的状态机流水线描述不需要像GPU那样考虑线程同步也不像CPU那样有取指开销。所有计算节拍是固定的延迟可精确到周期数。1.2 车牌检测识别流程如何映射到硬件流水线既然是整车牌流程都上FPGA那就要把算法步骤拆成硬件能接受的形式。车牌检测识别在PC上用OpenCV写步骤通常是灰度化、高斯模糊、边缘检测、形态学闭运算、轮廓查找、车牌粗定位、字符分割、字符识别。有些流程里还会加颜色筛选蓝色底、黄色底、绿底这些步骤在硬件上要重新设计。我最终落地的流水线是这样的第一步行缓冲3x3卷积做Sobel边缘检测同时用流水线对灰度图做中值滤波去噪这两个操作可以并行处理同一份图像数据流。第二步边缘图做水平和垂直方向的投影统计找车牌区域的候选框。传统的轮廓查找在RTL里非常麻烦循环嵌套深度不可控。投影法就友好得多只需要行计数、列计数和阈值比较全部可以流式完成。第三步根据车牌的宽高比例、字符区域占比等先验条件过滤候选框输出一个稳定的ROI区域坐标。第四步对ROI区域做垂直投影字符分割把7位车牌切成单字符。第五步把分割出来的字符归一化到固定尺寸比如32x32或28x28送入轻量CNN分类器CNN的卷积层用脉动阵列实现全连接层也映射成矩阵乘法同样在脉动阵列上跑。整条流水线从摄像头行数据开始到最后识别结果输出中间不存在需要CPU介入的环节。行缓冲、卷积、池化、脉动阵列、全连接全部以AXI-Stream或FIFO形式衔接数据流在FPGA内部是连续流动的不用等整帧图像“完全到达”再开始处理。这种流式处理正是低延迟的关键。1.3 为什么死磕纯Verilog不直接用HLS现在做FPGA AI加速很大一部分人首选用HLS因为开发效率高。但HLS有一个问题它生成的RTL代码在时序上不那么可控尤其是脉动阵列这种对每个PE内部节拍要求精确的结构HLS调度出来的逻辑往往有较多冗余状态综合后资源占用和时序都不如手写RTL干净。更关键的是脉动阵列的“数据泵”节奏需要精确到每一个周期手写Verilog可以确保PE内部的状态机最小化实现真正的全流水。纯Verilog还有一个实操层面的好处跨平台可移植性强。项目做到后半程我需要在Xilinx和紫光同创两个厂商的芯片上跑同一套代码。Xilinx的Vivado和紫光同创的PDSPango Design Suite虽然都是基于同样的综合工具底层但IP核生成方式、原语名称、约束语法都有差异。纯手写RTL几乎不依赖厂商原语只有时钟和IO需要做少量适配其他模块一套代码直接跑省掉大量移植时间。当然纯Verilog的代价就是开发周期长调试痛苦。但如果你想真正理解硬件加速的内部机制而不是只为“交个作业”手写RTL是必经之路。2. 核心模块参数设计与实操要点2.1 PE阵列尺寸与INT8定点方案脉动阵列设计的第一步是定尺寸和数据类型。这个决策直接影响资源占用、时序收敛难度和性能上限。我选的是8x8阵列数据位宽为8bit定点INT8。为什么是8x8而不是16x16也不是4x4来算一笔账。8x8阵列是64个PE每个PE内部有一个乘法器和一个加法器对应64个DSP或者64组LUT逻辑乘加。在Artix-7上DSP48E1数量足够PGL22G上逻辑资源也扛得住。16x16阵列虽然计算能力强4倍但每个PE都要从相邻PE收数据、向相邻PE发数据布线资源消耗剧增对100MHz以上的时序收敛压力很大。对于车牌识别这种小型CNN算力瓶颈本来就不高8x8带来的等效算力已经足够。INT8定点是图像加速的常规选择。车牌识别对分类精度要求没有那么苛刻字符识别类别数也就几十个数字字母省份简称INT8量化后精度损失很小。定点数处理时需要特别注意符号扩展两个INT8数相乘结果是16bit多个部分和累加之后需要防溢出累加器至少做到20bit以上在最后输出之前再截断到8bit或16bit。我在实际代码里部分和累加器用了24bit位宽最终结果根据后续模块需要截位。关于乘法器的实现Xilinx首选DSP48E1紫光同创也有类似的DSP硬核。但是为了移植方便我在RTL里直接用assign product a * b;让综合工具推断。两个平台都支持这种写法且都能正确推断到DSP。有一点值得注意紫光同创PDS对乘法器推断的默认行为跟Vivado不完全一样综合选项里需要确认打开了“Multiply-Add/DSP inference”相关的开关否则乘法和加法会被拆成纯LUT逻辑资源直接爆掉。2.2 脉动阵列的数据流权重驻留、输入推移、部分和累加脉动阵列的三种数据流模式中我选了最经典的权重驻留Weight Stationary模式。顺带说说为什么不用另外两种。输入驻留Input Stationary适合卷积核小、输入特征图复用的场景输出驻留Output Stationary适合输出特征图反复更新的场景。车牌识别用的卷积核是3x3输入特征图每个像素被多个输出位置复用理论上输入驻留更合适。但权重驻留更好写、更容易理解时序而且对于一个固定了网络结构的场景权重提前加载完毕后输入数据只需要以固定节奏流入控制逻辑非常简单。那PE内部到底怎么运转我画一层逻辑来说权重加载阶段Weight Loading阵列上电或切换网络层时微控制器/状态机把权重按行写入每一个PE内部的权重寄存器。8行PE每行8个共64个权重寄存器。这个阶段花64个周期完成。输入特征图流入阶段Input Streaming输入数据从阵列左侧进入每个周期每个PE把收到的输入乘上自己的权重然后把输入数据继续传递给右边相邻的PE。也就是说同一份输入数据在阵列中横向流动被8个PE依次使用这是横向数据复用。部分和累加阶段Partial Sum Accumulation每个PE除了算乘法还要把上方PE传来的部分和加上自己的乘法结果然后传给下方PE。这样同一列的不同PE处理的是同一个输入图像位置上不同输出通道的卷积结果垂直方向的累加最终形成完整的输出特征图某个像素。上面这个流程里输入数据在一行内滚动部分和在一列内滚动两者在二维空间里天然形成了卷积计算的展开。3x3卷积要处理多行输入我通过行缓冲把特征图的三行并行喂入阵列再由状态机控制在3x3窗口内滑动逐窗口产生输出。代码层面PE核心的Verilog描述非常简洁大概就是always (posedge clk or negedge rst_n) begin if (!rst_n) begin acc 24d0; data_out 8d0; end else if (load_weight) begin weight_reg weight_in; end else begin acc acc $signed(data_in) * $signed(weight_reg); data_out data_in; // 传递给右侧PE if (flush) acc 24d0; end end我故意简化了信号但核心就这几行。实际工程里要注意$signed的位宽匹配必须一致否则仿真里面算得对综合之后可能悄悄变成无符号乘法。这个我后面在调试章节会展开讲。2.3 图像预处理加速行缓冲、Sobel与二值化的流水线写法在脉动阵列之前图像数据要先变成边缘图。这部分在FPGA里属于经典操作但很多人一上来就写计数器然后发现行缓冲和帧同步跟不上图像撕裂、错位频发。这里把核心要点讲透。行缓冲Line Buffer本质是一组移位寄存器。以3x3 Sobel为例我需要同时知道当前像素的上、中、下三行邻域数据所以至少要缓存两整行的灰度值。我用的实现方式是用三个8bit宽、一行像素深度如512字节的移位寄存器链每来一个像素时钟三行数据各自移位取每个链的最新3个值组成3x3窗口。这比用BRAM做FIFO更容易理解效率也不低。Sobel算子的两个方向水平Gx、垂直Gy各需要3x3系数一共9个乘法器但其中很多是乘1、乘2、乘0可以化简成加法和左移。乘2就是左移一位乘0直接跳过。所以硬件实现时两个方向的卷积可以省到几个加法器完成的规模算出来的Gx和Gy再取绝对值相加或者求平方和作为边缘强度。车牌字符边缘明显边缘强度超过阈值就打1否则打0得到二值边缘图。二值化之后我做了形态学上的“膨胀”处理做法也简单对边缘图再做一次3x3窗口的操作窗口内只要有一个像素是1输出就是1。膨胀两次可以让字符轮廓连成片方便后续投影定位。再配合垂直方向的列投影累加用计数器统计每一列的边缘点数量阈值高于设定值的就是字符区域。这里有个容易掉的坑行缓冲的输入和输出存在固有的流水线延迟如果后面模块直接按“当前像素坐标”去读边界信息会出现几行错位。我采取的方案是给整个预处理链路上的每个模块都带上统一的“行有效/列有效”标志而不是各自维护坐标计数器。这样只要在源头把有效信号和像素数据同步移位所有模块对“当前处理到哪个像素”的认知就是一致的错位问题自动消失。2.4 车牌定位、字符分割与识别状态机预处理链出来后要做车牌粗定位。我参考了车牌尺寸的比例先验国内蓝牌一般是440x140mm字符区域占比约 4:1。换算到图像像素宽高比在2.5到3.5之间都算候选。投影法在这时发挥了巨大优势边缘图上做水平投影选出连续的高密度行带这就是车牌可能存在的高度范围在这个高度范围内再做垂直投影继续找到字符区域的左右边界。两个维度都拿到后就能框出ROI。ROI剪切在实际硬件里不像软件那样“拷贝一块内存”而是通过控制状态机在扫描到ROI内部坐标的时候才把对应像素送入下游模块。这个状态机同时负责跳过ROI外部的行和列。为了加速我在扫描到ROI顶部边界后才开始使能输出离开底部边界后停止一切处理等待下一帧。字符分割我用的也是垂直投影在ROI内部对每一列统计边缘点数量字符笔画密集的列投影值高字符之间的间隙投影值接近零。这样按“高投影”连续段切分就得到7个左右字符块。实际踩过一个小坑车牌中间有小圆点投影后圆点的投影值往往介于字符和间隙之间。我通过宽度过滤解决字符块宽度小于正常字宽阈值或者高宽比异常时直接丢弃。小圆点因为面积小、宽高比不满足会被过滤掉。识别部分我训练了一个简化版CNN输入32x32单通道灰度字符第一层8通道3x3卷积8个输出通道接2x2最大池化输出16x16x8的特征图再接16个神经元的全连接层最后Softmax分类到类别数量如省份汉字数字字母总计约67类。卷积计算映射到脉动阵列上把输入窗口的数据按行流入阵列每列对应一个输出通道的权重8列正好对应8个输出通道。全连接层按矩阵乘法处理同样可以复用脉动阵列只需把权重矩阵加载进PE阵列输入向量从左侧流入。3. Xilinx与紫光同创FPGA的部署实录3.1 Xilinx Vivado工程搭建与底层约束先讲Xilinx侧。我用的芯片是Artix-7 XC7A35T速度等级-2这个芯片有约52000个LUT和90个DSP48E1。8x8脉动阵列、预处理链路、CNN分类器全部加起来LUT占用率在70%以上这说明Artix-7其实有点吃紧。如果大家做同样的项目上XC7A100T会更从容。工程结构按模块分目录src/rgb2gray、src/preprocess、src/systolic、src/cnn、src/uart顶层用top.v例化所有子模块。所有模块间握手指令只用valid/ready图像数据流用axis_valid/axis_ready/axis_data寄存器配置总线用简单的APB接口这样后续接软核或者上位机都方便。约束文件的几个要点时钟输入摄像头像素时钟或者外部晶振时钟先经过MMCM/PLL生成单一逻辑时钟全工程统一用这个主时钟。我跑的是100MHz这个频率对8x8阵列和所有流水线逻辑来说时序收敛压力不大。复位统一用异步复位、同步释放复位信号要经过BUFG进入全局时钟网络。引脚约束摄像头DVP接口的PCLK、HSYNC、VSYNC、D[7:0]直接约束到对应引脚UART_TX、UART_RX约束到板载USB转串口引脚。时序例外跨时钟域如摄像头PCLK域到逻辑主时钟域的FIFO格雷码指针之间加上set_false_path串口波特率时钟域之间同理。Vivado综合时我习惯把默认策略改为Performance_Explore然后用Phys_Opt_Design跑一轮物理优化。脉动阵列的关键路径往往出现在乘法器的部分和累加链上原因是多个PE垂直方向串联累加链每个周期都在变化。解决办法是在阵列内部插入流水寄存器每两个PE之间放一组寄存器避免单个时钟周期内累加链穿过整个阵列。代价是输出延迟增加几个周期但换来的是时序收敛轻松很多。3.2 紫光同创PDS移植比想象中简单但有几个暗坑紫光同创的PGL22G是我后期验证的这块芯片的逻辑资源约等于Xilinx Artix-7的入门款。PDSPango Design Suite的整体流程跟Vivado很像create project、add sources、constraints、synthesis、place route、programmer步骤是通的。最让人省心的是我整个工程除了顶部时钟原语之外几乎没有任何厂商相关性RTL直接导入就能综合。但以下差异必须注意第一原语名称不同。Xilinx的PLL叫PLLE2_BASE紫光同创的叫PLL参数化接口也不同。我的做法是把时钟模块单独建一个文件两个平台各维护一份用宏USE_PANGO切换ifdef USE_PANGO PLL u_pll ( .clk_in(pix_clk), .clk_out(logic_clk), .lock(pll_lock) ); else PLLE2_BASE #(...) u_pll (...); endif第二约束语法不同。Vivado用XDCPDS用PDC虽然是类似格式但引脚位置描述、IO标准设置的命令名略有差异。另外PDS里IO约束默认不是LVCMOS33需要显式指定IOBUF的类型。第三DSP推断选项。我前面提到过PDS里纯RTL乘法器可能被推断到LUT。在综合设置里找到“DSP inference level”至少要选Auto或Medium。我实测不设置这个选项乘法器全部变LUT占用率从35%直接飙到80%时序直接崩掉。第四若干综合问题表现不同。PDS对casez、generate循环的某些写法支持略有差异我在Xilinx上能过综合的代码PDS偶尔会报“部分索引表达式不是常量”。解决办法是把这类代码改成显式展开的generate for或直接用always块展开写代码时尽量别依赖过于花哨的语法。3.3 板级验证从ILA看到UART输出车牌字符串Xilinx侧的调试我用Vivado的ILAIntegrated Logic Analyzer抓关键信号。推荐抓这四组信号脉动阵列的data_in/valid_in部分和累加器的输出CNN全连接层的输出以及UART发送模块的tx_done。看到的数据流与仿真一致基本就能确认链路已经通。抓信号时有个细节ILA的采样深度不要太大不要直接采样整个脉动阵列的所有PE内部信号否则布线时插入大量探针时序容易变差。尽量只抓阵列输入和输出接口内部状态靠仿真确认。这样上板实测的时序才是真实时序。紫光同创PDS里也有逻辑分析仪叫PDS Logic Analyzer用法类似。PGL22G板子上没有板载USB调试器所以调试时用UART输出中间状态设计里加一个debug_mux把ROI坐标、字符分割结果、每个字符的分类id通过UART发送到PC串口助手。这个方法虽然土但排查逻辑问题非常高效不用在布线复杂的ORCA调试器上纠结。最终上板验证时把摄像头对准一张标准蓝底白字车牌图片或真实的停车场车辆串口输出的一行字符串类似[SUCC] Plate: 京A12345。整个流程从摄像头帧同步信号进来之后到UART发送启动我花2~3分钟连续拍摄多辆车识别稳定率95%以上室内静态场景单帧延迟实测2.8ms左右100MHz。这个数据跟PC端OpenCV动辄几十毫秒的处理相比速度优势是很明显的。4. 调试经验、常见坑与性能分析4.1 仿真和Testbench哪个信号错了最难查建议把Testbench按模块拆散不要只写一个巨大的顶层TB。脉动阵列的TB应该尽可能简单给一组已知权重和输入验证输出跟手算结果完全一致。比如输入1x8向量[1,2,3,4,5,6,7,8]8个PE权重都设为1那么第一列输出应该是部分和逐行累加的结果。手算清楚再去对仿真波形能快速定位PE内部累加顺序对不对。我调试中最难查的一个问题是输出结果整体右移了一拍。现象是CNN识别结果总比预期少一个数但只差一拍波形上看不出大问题。后来发现是输入特征图在流入阵列之前经过了一个流水线寄存器而权重加载时机没有同步推迟导致第一个数据被当成无效数据跳过。查这个问题用了一下午其实就是对齐问题所以在这里专门提醒大家脉动阵列所有数据通道和控制信号之间延迟匹配必须是设计时就要想清楚的事情。对于验证CNN功能我写了一个脚本式TB用Python读出一张处理好的32x32字符位图转成HEX文件Testbench用$readmemh加载到RAM作为CNN输入仿真结束后把软输出通过$fwrite写出到文本再和Python端软浮点模型的结果做对比。这个对比不需要完全相等允许量化误差但分类id必须一致。这个方法可以快速验证RTL计算有没有结构性错误。4.2 时序收敛和技术上的取舍这套设计在100MHz下收敛没有太大困难但如果想往上超频卡点几乎都在脉动阵列内部。前面提到过部分和累加链是从上往下穿过多行PE的链越长组合逻辑越深。8行PE串联如果不插流水寄存器光加法链delay就能到十几纳秒100MHz周期10ns必定违例。解决办法是两层第一阵列内部做流水寄存器每两行PE之间插一拍第二累加器本身不要做成“把上一个PE结果直接加当前乘法结果”的组合逻辑链而是让每个PE的累加器自己打一拍所有PE的累加操作并行执行。这样关键路径缩短为“单个PE的乘法器输出累加器加法寄存器建立时间”8行串联的问题被完全消除。此外对FPGA上的BRAM使用也要提前规划。预处理阶段的行缓冲和ROI缓存我各自分配了单独的BRAM避免多端口读写冲突。脉动阵列的权重存储由于数据量小直接用分布式RAMLUT-RAM实现。CNN的特征图缓存用Block RAM按双端口设计一边写输入、一边读输出。4.3 两个平台都适用的常见问题速查表现象可能原因排查与解决上板图像整体错位几行/几列行缓冲有效信号与像素数据不同步检查有效标志是否与像素数据同步打拍改用统一的valid信号链脉动阵列输出恒为0PE权重没有正确加载检查权重加载状态机的地址顺序确认load_weight脉冲只拉高一次输出结果是预期值的2倍INT8乘法符号扩展没做检查$signed位宽匹配乘法后累加器位宽要足够CNN分类结果飘忽不定输入数据未归一化或截位溢出检查定点截位策略累加器是否在输出前做了饱和截断紫光同创综合后LUT爆满DSP推断选项未开启在综合设置里启用乘法器/DSP推断重跑综合UART输出乱码系统时钟域与波特率时钟域未隔离对UART模块用独立的计数器分频或加异步FIFOILA抓信号后退化探针太多导致布局布线恶化减少探针数量优先抓接口而非内部信号排查的效率直接跟设计的可观测性挂钩。我强烈建议在顶层模块里预留一组调试输出寄存器用板上的拨码开关选择要观测的信号接到LED或者UART。这样不用每次改代码重编译就可以快速定位到底是哪一级模块出了问题。4.4 性能到底怎么样延迟和资源实测数据给一组实测参考数据方便大家评估自己的方案能做到什么水平图像分辨率640x480灰度图逻辑时钟100MHz约10ns周期端到端延迟从VSYNC帧同步信号开始到UART发送完成车牌字符串的第一个字节延迟约2.8ms动态场景下略有波动因为ROI位置会影响字符分割的长度但最坏不超过3.5ms吞吐能力按帧间间隔计算纯流水线处理帧率可以远高于30fps受限于我的摄像头接口是8bit并行DVP实测帧率约30~50fps资源占用XC7A35TLUT 71%、FF 48%、BRAM 9/50、DSP 37/90资源占用PGL22G逻辑单元约78%、DSP约40%数字记不太清大致这个量级这套数据说明脉动阵列结构在小型FPGA上应付小车牌识别是够用的而且预算非常充裕。如果要做多个车牌同时识别或者处理1080p分辨率那就要考虑更大规模的阵列和外部存储DDR3了因为行缓存和特征图缓存的BRAM占用会明显增加。5. 这类项目往深了走还能怎么扩展脉动阵列本身是一个通用计算内核不只能做车牌识别。换成不同加载权重同样一套RTL就能跑边缘检测、图像分类、简单目标检测。如果你想在这个项目基础上继续深入学习以下几个方向值得试一下第一个方向是加大尺度。把8x8阵列扩到16x8或16x16处理更大分辨率的图像跑更深一点的CNN。这个过程中最核心的挑战不是写RTL而是数据搬运。小阵列时权重可以直接放在LUT-RAM里但大阵列的特征图要用DDR3缓存就需要在脉动阵列和DDR3控制器之间加DMA引擎和Tiling调度逻辑。这个方向会让你接触到完整的存储层次设计对理解计算机体系结构有巨大帮助。第二个方向是提高数据并行度。目前的阵列做的是INT8单精度如果改成INT4甚至更低比特可以用一个DSP实现多个乘法运算算力密度进一步提升。但量化带来的精度损失要重新评估车牌数据集小可能还好换到更复杂任务时就要仔细做量化训练。第三个方向是接更先进的接口。把DVP摄像头换成MIPI CSI接口或者通过FMC子板接入高速ADC/LVDS数据源让设计脱离开发板的限制变成真正可以放进实际设备里的完整系统。到这个阶段你对FPGA的理解就不再只是“写RTL逻辑”而是涵盖接口协议、时钟架构、板级调试的全栈能力。最后一个建议如果你也想尝试这个项目一定要在前期就把架构图画清楚特别是数据流的方向和节拍不要急着写代码。脉动阵列这个结构你真正动手写RTL之前理解得越透彻后面调试就越顺。把它想象成一个工厂流水线什么时候送料、什么时候加工完传出去节奏是铁的规律。写成代码之后再去改架构代价太大了。我自己在做这个项目的过程中几次想偷懒用HLS最终还是忍住了。现在回头看纯Verilog带来的收益很明确每一级流水延迟心里有数两个平台的部署过程也几乎没有遇到“换平台就黑屏”的问题。如果你也在用FPGA折腾图像处理或AI加速希望这篇记录能帮你少踩几个坑。后面我打算把CNN模型换成8位量化训练后校准的版本再试试把延迟压到1ms以内。有结果再来更新。