
1. 为什么我放弃了在ZYNQ上跑Linux做人脸识别这条路先说结论如果你手上有一块ZYNQ7000开发板想做一个能实时识别人脸的智能摄像头最省事、最稳、延迟最低的方案不是装Ubuntu、不是编译OpenCV、更不是折腾各种深度学习推理框架而是直接用PYNQ 2.6.0的Overlay机制把PL端当协处理器用PS端只做调度和显示。这套方案我从头到尾跑通之后端到端延迟压到了40毫秒以内功耗不到5瓦整块板子塞进一个3D打印外壳里就能当独立设备用。很多人一提到ZYNQ做人脸识别第一反应是ARM核跑LinuxOpenCV走起。我一开始也是这么想的结果踩了一堆坑交叉编译OpenCV花了整整两天Haar级联检测器在ARM A9上跑640x480的图帧率只有3到5帧画面卡得像幻灯片。后来换DNN模块加载Caffe模型内存直接爆了ZYNQ7010只有512MB DDR3根本扛不住。更别提那些需要CUDA的推理框架ZYNQ根本没有GPU。转折点是我重新审视了ZYNQ的架构本质。ZYNQ7000系列的核心价值在于PL可编程逻辑和PS处理系统的紧耦合AXI总线带宽能到几个GB每秒PL端做像素级并行运算的效率是ARM核的几十倍甚至上百倍。人脸识别的前端处理——灰度化、直方图均衡、积分图计算、Haar特征扫描——这些全是规则化的并行运算天生适合FPGA。而PS端只需要做最后的分类决策和结果输出负载很轻。PYNQ 2.6.0这个版本对我来说是个甜点版本。它基于Ubuntu 18.04Python 3.6Jupyter Notebook开箱即用Overlay加载机制已经非常成熟。最关键的是PYNQ的pynq.Overlay类可以直接把编译好的.bit和.hwh文件加载进PL然后通过MMIO或者DMA跟PL端交互完全不需要写内核驱动。这意味着我可以在Jupyter里用Python控制FPGA的硬件加速器开发效率比传统FPGA流程高了不止一个量级。所以这篇文章要讲的就是怎么用ZYNQ7000加PYNQ 2.6.0从零搭一个能实时识别人脸的智能摄像头。我会把PL端的加速器设计思路、PS端的调度逻辑、摄像头数据的搬运方式、以及我实际调试中踩过的坑全部摊开讲。适合有FPGA基础但没怎么碰过PYNQ的开发者也适合玩过树莓派人脸识别但想升级到硬件加速的朋友。2. 硬件选型与PYNQ 2.6.0环境搭建的取舍细节2.1 为什么选ZYNQ7010而不是7020或者树莓派市面上常见的ZYNQ7000开发板有7010和7020两个主力型号。7010的PL端有28K逻辑单元7020有85K。乍看7020资源多得多但我实测下来做人脸检测这个任务7010完全够用而且便宜不少。我的PL端设计里最大的资源消耗是积分图计算模块和Haar特征扫描模块。积分图需要一行BRAM做行缓存640像素宽、8位灰度一行就是640字节用两个BRAM18就能搞定。Haar特征扫描是纯组合逻辑加加法树不消耗BRAM。整个设计跑下来LUT用了大概12000个FF用了8000个BRAM用了36个DSP用了12个。7010的28K逻辑单元、60个BRAM、80个DSP完全撑得住。7020的优势在于如果你要做更复杂的任务比如同时跑人脸检测和人脸识别特征提取加比对资源会更宽裕。但如果你只是做检测7010性价比更高。至于树莓派我承认它做人脸识别很方便OpenCV加Python几行代码就搞定。但树莓派的瓶颈在CPU实时性差而且功耗高。ZYNQ的PL端做像素处理是硬件并行延迟是确定性的不受操作系统调度影响。如果你要做的是工业级或者需要低延迟的应用ZYNQ是更合适的选择。2.2 PYNQ 2.6.0镜像的烧录与首次启动PYNQ 2.6.0的镜像文件大概4GB左右烧录到MicroSD卡上。我用的是SanDisk的32GB Class10卡烧录工具用balenaEtcher写完之后把卡插到板子上接上网线、串口线、电源上电。首次启动大概需要两分钟串口会打印启动日志。默认的IP是192.168.2.99用户名和密码都是xilinx。如果你不知道板子的IP可以在路由器后台看或者用串口登录之后用ifconfig查。启动完成之后在浏览器里输入http://192.168.2.99:9090就能看到Jupyter Notebook的界面。PYNQ 2.6.0默认带了很多示例Notebook包括GPIO控制、DMA传输、Overlay加载等建议先跑一遍这些示例熟悉一下PYNQ的基本操作。注意PYNQ 2.6.0的Jupyter默认密码是xilinx第一次登录后建议改掉。另外如果你要用摄像头需要确认板子的USB接口能供电有些便宜的USB摄像头功耗比较大可能需要外供电的USB Hub。2.3 摄像头选型与USB带宽的坑我试过三种摄像头罗技C270、微软LifeCam HD-3000、以及一款国产的免驱USB摄像头。结论是罗技C270最稳UVC协议兼容性好PYNQ的OpenCV能直接通过cv2.VideoCapture(0)打开。但这里有个坑ZYNQ7010的USB控制器是USB 2.0理论带宽480Mbps实际能用的也就200Mbps左右。640x480的YUV格式30帧每秒数据量是640x480x2x30 18.4MB/s约147Mbps勉强够用。但如果你开MJPG格式虽然带宽低但PS端解码MJPG会消耗CPU帧率反而上不去。我的建议是摄像头输出YUYV格式分辨率设640x480帧率设30。PS端用OpenCV读取一帧转成灰度然后通过DMA搬到PL端做处理。这样PS端的负载最小PL端拿到的是干净的灰度图。3. PL端人脸检测加速器的设计思路与关键模块3.1 为什么选Haar级联而不是深度学习深度学习做人脸检测精度确实高但ZYNQ7010的PL端资源有限跑CNN需要大量的DSP和BRAM7010根本放不下。而且深度学习模型的权重需要从DDR加载带宽压力大延迟也不确定。Haar级联是经典算法虽然精度不如深度学习但在正面人脸检测这个场景下配合合适的参数检出率能到95%以上误检率也能接受。更重要的是Haar特征的计算全是加法和减法非常适合FPGA的并行架构。我的设计里PL端实现了一个完整的Haar级联检测流水线灰度图输入、积分图计算、Haar特征扫描、级联分类器判决、非极大值抑制、结果输出。整个流水线是硬件并行的一帧640x480的图从输入到输出结果延迟不到10毫秒。3.2 积分图模块的BRAM行缓存设计积分图是Haar特征计算的基础。对于像素点(x,y)积分图的值是原图中所有满足xx且yy的像素之和。用公式表示就是ii(x,y) sum_{xx, yy} i(x,y)直接计算积分图需要遍历所有前面的像素复杂度是O(N^2)。但用递推公式可以降到O(N)ii(x,y) i(x,y) ii(x-1,y) ii(x,y-1) - ii(x-1,y-1)在FPGA里实现这个递推需要缓存上一行的积分图值。我的做法是用两个BRAM18做行缓存每个BRAM18是18Kb能存2048个8位数据。640像素宽一行积分图的值最大是640x480x255需要32位来存。所以实际上我用的是两个BRAM36配置成32位宽、1024深度的双端口RAM。行缓存的读写时序需要仔细设计。积分图计算是逐行进行的当前行的计算需要上一行的积分图值。我用了一个乒乓缓冲的结构计算第N行时从BRAM_A读第N-1行的积分图同时把第N行的积分图写入BRAM_B。下一行反过来。这样读写不冲突能跑到100MHz以上的时钟。实操心得BRAM的读延迟是1个时钟周期写是同步的。在递推公式里ii(x-1,y)是当前行左边的值可以用一个寄存器缓存ii(x,y-1)是上一行同列的值从BRAM读ii(x-1,y-1)是上一行左边的值也需要从BRAM读。这三个值的读取时序要对齐否则算出来的积分图会错位。我一开始没注意这个调试了两天才发现是BRAM读延迟导致的。3.3 Haar特征扫描的并行化策略Haar特征本质上是几个矩形区域的像素和之差。有了积分图任意矩形的像素和可以在4次查表和3次加减法之内算出来。对于矩形(x,y,w,h)像素和是sum ii(xw, yh) - ii(x, yh) - ii(xw, y) ii(x, y)一个Haar特征通常由2到4个矩形组成所以一个特征的计算需要8到16次查表和若干次加减法。在FPGA里我可以并行放置多个特征计算单元每个单元独立计算一个特征然后汇总。我的设计里放了16个并行的Haar特征计算单元每个单元负责一个特定位置和尺度的特征。扫描窗口是24x24像素这是Viola-Jones论文里的标准窗口大小。对于640x480的图扫描窗口需要滑动(640-24)/2 x (480-24)/2 308 x 228个位置每个位置计算大约200个特征。总共是308x228x200 1400万个特征计算。如果串行做100MHz时钟也要0.14秒太慢了。并行化之后16个单元同时工作每个时钟周期能算16个特征1400万/16 87.5万个时钟周期100MHz下是8.75毫秒。再加上积分图计算和其他开销一帧的处理时间在10毫秒左右能跑到100帧每秒。当然实际受限于摄像头帧率和DMA带宽最终系统帧率在30帧左右。3.4 级联分类器的判决逻辑与阈值存储Haar级联分类器是由多个强分类器级联而成的。每个强分类器由若干个弱分类器就是Haar特征加阈值组成。检测时如果某个强分类器的输出低于阈值就直接判定为非人脸后面的强分类器不用算了。这个early rejection机制能大幅减少计算量。在FPGA里我把每个强分类器的阈值存在BRAM里弱分类器的Haar特征参数矩形位置、权重也存在BRAM里。判决逻辑是一个状态机从BRAM读出当前强分类器的所有弱分类器参数计算每个弱分类器的输出累加然后跟阈值比较。如果通过进入下一个强分类器如果不通过输出非人脸复位状态机。注意BRAM的容量有限7010只有60个BRAM18。我的级联分类器有25个强分类器总共约3000个弱分类器每个弱分类器需要存储4个矩形的坐标和权重大概20字节总共60KB。这超过了BRAM的容量。我的解决方案是把分类器参数存在DDR里PL端通过AXI Master接口按需读取。虽然增加了延迟但DDR的带宽足够实际影响不大。4. PS端与PL端的协同DMA搬运与Python调度4.1 AXI DMA的配置与Scatter-Gather模式PS端和PL端的数据搬运我用的是Xilinx的AXI DMA IP核。DMA有两种模式Simple模式Direct Register Mode和Scatter-Gather模式。Simple模式适合连续的大块数据传输Scatter-Gather模式适合分散的数据块。我的场景是PS端从摄像头读一帧转成灰度存在DDR的一个缓冲区里然后启动DMA把缓冲区里的数据搬到PL端的流接口PL端处理完之后把结果写回另一个DDR缓冲区PS端读结果画框显示。这个流程用Simple模式就够了。DMA的配置很简单设置源地址、目的地址、传输长度然后启动。传输完成之后DMA会产生中断PS端在中断处理函数里读结果。在PYNQ里DMA的配置通过pynq.lib.dma.DMA类来完成。代码大概长这样from pynq import Overlay, allocate from pynq.lib.dma import DMA ol Overlay(face_detect.bit) dma ol.axi_dma_0 input_buffer allocate(shape(640*480,), dtypenp.uint8) output_buffer allocate(shape(640*480,), dtypenp.uint8) # 填充input_buffer # ... dma.sendchannel.transfer(input_buffer) dma.recvchannel.transfer(output_buffer) dma.sendchannel.wait() dma.recvchannel.wait()allocate函数会分配物理连续的内存这是DMA传输的前提。如果内存不连续DMA会传输出错。4.2 摄像头帧采集与灰度转换的PS端优化PS端从摄像头读帧我用的是OpenCV的cv2.VideoCapture。但这里有个性能问题OpenCV读出来的帧是BGR格式转灰度需要调用cv2.cvtColor这个操作在ARM A9上大概需要5到8毫秒。如果每帧都转30帧每秒就是150到240毫秒的CPU时间占了总时间的15%到24%。我的优化方案是让摄像头直接输出YUYV格式然后用numpy做灰度转换。YUYV格式里Y分量就是灰度值直接取出来就行不需要做加权平均。代码ret, frame cap.read() gray frame[:, :, 0] # YUYV格式的Y分量在第一个通道这样转换时间几乎为零。但要注意OpenCV读出来的YUYV帧是两通道交错的需要reshape一下。具体操作frame frame.reshape(480, 640, 2) gray frame[:, :, 0]4.3 检测结果的后处理与画框显示PL端输出的检测结果是一组矩形框的坐标每个框用4个16位数表示x, y, w, h。PS端读到这些坐标之后用OpenCV在原图上画矩形框然后通过HDMI或者USB摄像头回传显示。画框本身很快cv2.rectangle在ARM上大概0.1毫秒一个框。但如果检测到的人脸多比如10个框也就1毫秒。显示方面如果你用HDMI输出PYNQ的显示接口是直接映射到DDR的framebuffer写进去就行。如果用USB摄像头回传需要把画好框的图编码成MJPG这个比较耗时大概10到15毫秒一帧。我的建议是如果只是做原型验证直接在Jupyter里用matplotlib显示就行虽然帧率低但方便调试。如果要做成独立设备用HDMI输出延迟最低。5. 实测性能与踩坑记录5.1 端到端延迟的测量与瓶颈分析我在系统里加了一个GPIO引脚PS端在开始处理一帧时拉高处理完拉低用示波器测脉宽。实测下来端到端延迟平均42毫秒其中阶段耗时摄像头采集8ms灰度转换与DMA搬运6msPL端Haar检测10ms结果回传与画框3ms显示输出15ms最大的瓶颈是显示输出。如果用HDMI直接写framebuffer显示延迟能降到5毫秒以内。但HDMI的驱动在PYNQ 2.6.0里配置起来比较麻烦需要改设备树。我暂时用的是USB摄像头回传延迟高一些但够用。5.2 BRAM时序违例导致的检测结果错乱这个坑我踩得最深。PL端设计综合实现之后时序报告显示BRAM的建立时间有违例slack是-0.5ns。我当时没在意觉得差一点点没关系。结果上板之后检测结果时对时错有时候能检测到人脸有时候检测不到而且错的时候是整帧都错。排查过程先用ILA集成逻辑分析仪抓积分图模块的输出发现积分图的值在某些行会突然跳变。对比MATLAB的参考模型发现是BRAM读出来的上一行积分图值不对。原因是BRAM的读延迟在时序违例的情况下变成了2个时钟周期而我的状态机是按1个时钟周期设计的。修复方案在BRAM的读地址上打一拍让读延迟变成确定的2个时钟周期然后调整状态机的时序。重新综合之后slack变成0.3ns上板测试检测结果稳定了。实操心得FPGA设计里时序违例绝对不能忽视哪怕slack只差0.1ns。因为时序违例的行为是不确定的可能今天能跑明天温度一变就挂了。BRAM的读延迟一定要在设计时确认清楚是1拍还是2拍然后按最坏情况设计。5.3 PYNQ Overlay加载失败的常见原因PYNQ加载Overlay的时候有时候会报Overlay not found或者Bitstream download failed。我遇到过几次原因各不相同第一次是.hwh文件和.bit文件不匹配。.hwh是Vivado导出的硬件描述文件包含了PL端的IP核信息。如果Vivado工程改了但没重新导出.hwhPYNQ就找不到对应的IP核。解决方法是每次改完Vivado工程都要重新导出.hwh和.bit。第二次是bitstream的时钟配置不对。PYNQ默认的Overlay时钟是100MHz但我的设计里用了150MHz的时钟。加载的时候PYNQ会尝试配置时钟如果配置失败就会报错。解决方法是在Vivado里把时钟配置成PYNQ支持的频率或者在PYNQ的代码里手动配置时钟。第三次是DDR内存不足。PYNQ加载Overlay的时候需要分配DMA缓冲区如果DDR剩余空间不够就会失败。ZYNQ7010只有512MB DDRLinux跑起来之后剩大概300MB。如果同时开Jupyter、OpenCV、还有DMA缓冲区很容易不够。解决方法是关掉不必要的服务或者用更小的缓冲区。5.4 摄像头掉帧与USB带宽的实测数据我用罗技C270在640x480分辨率下测试YUYV格式理论帧率30fps。但实际跑下来平均帧率只有22到25fps偶尔会掉到15fps。用v4l2-ctl查看USB带宽占用发现峰值到了180Mbps接近USB 2.0的实际上限。优化方案把分辨率降到320x240帧率立刻稳定在30fps而且PL端的处理时间也降到了3毫秒端到端延迟降到了25毫秒。虽然分辨率低了但人脸检测在320x240下依然能正常工作因为人脸在画面里占的比例够大。如果你非要用640x480建议用MJPG格式带宽能降到50Mbps左右。但PS端解码MJPG需要CPU帧率反而可能更低。我的建议是根据实际需求权衡如果检测距离近320x240够用如果需要检测远处的人脸640x480更合适但要接受帧率波动。6. 从原型到产品还可以怎么优化6.1 用PYNQ的Overlay机制做多算法切换PYNQ的Overlay机制允许你在运行时动态加载不同的bitstream。这意味着你可以做多个Overlay一个做人脸检测一个做人脸识别特征提取一个做手势识别。根据应用场景切换。具体做法是把每个算法做成独立的Vivado工程导出各自的.bit和.hwh。在Python里用Overlay类加载不同的文件。切换时间大概1到2秒因为要重新配置PL。这个机制的好处是资源复用。7010的PL资源有限不可能同时放下所有算法。但通过分时复用你可以用一块板子实现多种功能。6.2 把检测结果通过MQTT推送到上位机如果你要做成物联网设备可以把检测结果通过MQTT推送到上位机。PYNQ的Linux里有Python的paho-mqtt库直接pip安装就行。代码大概长这样import paho.mqtt.client as mqtt import json client mqtt.Client() client.connect(broker_ip, 1883, 60) def publish_detection(faces): payload json.dumps({count: len(faces), boxes: faces}) client.publish(face/detection, payload)这样上位机就能实时收到人脸检测的结果做进一步的统计分析或者联动控制。6.3 低功耗优化动态时钟与电源门控ZYNQ7010的功耗本来就不高满载大概2到3瓦。但如果做电池供电的设备还能进一步优化。PL端的时钟可以在没有帧输入的时候降到10MHz有帧的时候再升到100MHz。PS端可以用cpufreq调频把CPU频率从666MHz降到333MHz。电源门控方面ZYNQ支持PL端的部分重配置可以把不用的逻辑区域关掉。但这个比较复杂需要Vivado的Partial Reconfiguration流程我还没深入折腾。6.4 用PYNQ-Z2板载的HDMI做低延迟显示PYNQ-Z2板子上有HDMI输出接口直接连到PL端的HDMI IP核。如果你用这个接口做显示延迟能降到5毫秒以内因为数据不经过USB直接写framebuffer。配置HDMI需要改设备树把HDMI IP核的地址映射到Linux的framebuffer设备。具体步骤在Vivado里加HDMI IP核导出.hwh然后在PYNQ的Linux里用fbset配置分辨率。这个我还在折腾等跑通了再分享。最后分享一个我在调试过程中总结的小技巧PL端的设计一定要加ILA集成逻辑分析仪哪怕资源紧张也要加。因为PL端的信号用示波器测不到只能靠ILA抓。我每次上板调试第一件事就是打开ILA抓关键信号看时序。没有ILA调试FPGA就像盲人摸象效率极低。