ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

PYNQ-Z2上的HLS矩阵乘法加速:从零到Python调用完整实践

2026/10/6 3:33:46 拓冰建站 浏览量
PYNQ-Z2上的HLS矩阵乘法加速:从零到Python调用完整实践 第一次接触PYNQ-Z2的HLS开发十有八九会被“用C写FPGA”这个概念吸引住既想保留Python调硬件的爽快又不想回头去写Verilog。现实也很骨感网上教程要么只讲单个环节要么把所有步骤打散真正从零一路跑到板子亮灯、跑通数据、拿到加速比的内容很少。这篇文章不煮鸡汤也不复制官方文档直接把我从搭建HLS流程、写矩阵乘法加速器、集成到PYNQ、最后在Jupyter里用Python调用硬件的全过程和踩过的坑一次说清楚。[!NOTE] 整套流程适合三类人刚入手PYNQ-Z2不知道下一步做什么的入门者已经会用Verilog但想提高开发效率的老手以及想评估“算法直接转硬件”到底靠不靠谱的软件工程师。1. 为什么PYNQ-Z2上的HLS开发值得上手板子与工具的选型思路1.1 PYNQ-Z2这块板子的家底PYNQ-Z2的核心是Xilinx Zynq-7000 SoC具体型号是XC7Z020-1CLG400C。它内部不是一颗单独的FPGA而是把双核ARM Cortex-A9处理器PS侧和Artix-7架构的可编程逻辑PL侧封装在了同一颗芯片里。这意味着跑Linux系统的ARM处理器和做并行计算的FPGA逻辑可以共享同一片DDR内存通过AXI总线高速互相访问。PYNQ这个框架真正的价值是把PL侧当成一个“可以动态加载的硬件库”。PYNQ-Z2官方镜像启动后直接进入Jupyter Notebook你可以用Python加载“overlay”也就是一个已经综合好的比特流文件然后像调用numpy一样调用FPGA上做好的功能模块。这种玩法让FPGA不再只是硬件工程师的工具也成了做嵌入式视觉、信号处理、加速算法原型验证的软件工程师的实验台。但这里有个隐藏边界PYNQ本身只管加载、控制、数据搬运管不了你如何在PL侧造出一个自定义加速器。传统做法是你去学一套Verilog/VHDL工作量不小HLS则把这条边界抬高了允许你用C/C描述算法再自动生成RTL逻辑。1.2 HLS解决什么问题从“写逻辑”到“写算法”HLSHigh-Level Synthesis高层次综合的价值用一句话概括把用C/C描述的算法转换成可以在FPGA上运行的寄存器传输级RTL电路。传统FPGA开发里你要先明确每个时钟周期、每个信号线、每个状态机的行为HLS开发里你更多关注算法本身循环怎么流水、数组怎么并行、数据怎么搬运这些底层细节通过综合指令pragma来引导工具完成。这不是说硬件工程师就没用了而是节奏完全不同。用Verilog实现一个矩阵乘法状态机写半天用HLS核心循环几十行C代码就能表达然后通过#pragma HLS PIPELINE和#pragma HLS ARRAY_PARTITION这类指令把循环展开、流水、数组切分到BRAM/寄存器的优化空间里。综合工具会给你产生一个带AXI接口的IP核供Vivado Block Design直接调用。HLS当然不是万能的不是所有代码都能被综合动态内存分配和递归基本不可综合时序约束紧、资源占用极紧的模块手写RTL仍然更可控。但对绝大多数“算法加速”场景HLS带来的开发效率提升是实打实的我后面会用一个完整例子验证。1.3 为什么选PYNQ-Z2而不是别的平台做HLS如果你已经有其他FPGA开发板理论上HLS流程是通用的跟着Vivado HLS/Vitis HLS走就行。但PYNQ-Z2有它不可替代的优势硬件验证环节被Python无缝接管。Vivado里生成的比特流和hwh文件放到PYNQ的overlay目录Jupyter里三行代码就能加载并测性能。即使你对Linux不熟也不用额外去写驱动和用户态程序PYNQ把寄存器的映射和DMA缓冲区管理都封装好了。对HLS学习来说快速反馈非常重要。传统FPGA开发中综合、布局布线、烧写、调试一轮下来几十分钟起步PYNQ-Z2上HLS仿真阶段可以在PC上确认算法板上验证阶段Jupyter交互式执行省下来的时间全部用在优化算法上。这也是我劝新手从PYNQ-Z2入FPGA HLS开发的原因门槛低、反馈快、生态资源多。2. 零基础起步PYNQ-Z2的HLS开发环境搭建与工程模板2.1 工具链准备Vivado、Vivado HLS/Vitis HLS和镜像做PYNQ-Z2 HLS开发主要涉及两套环境开发机环境安装Xilinx Vivado含HLS工具。早期版本叫Vivado HLS是一个独立图形界面2020.1之后的版本把它并入Vitis统一叫Vitis HLS基本功能一致。我的方案是使用Vivado 2020.1对应Vitis HLS 2020.1和PYNQ-Z2官方v2.6/v2.7镜像兼容性很好。板卡环境给PYNQ-Z2烧录官方镜像。去PYNQ官网下载对应SD卡镜像用rufus等工具写入至少8GB的microSD卡。上电后通过网线连接路由浏览器访问pynq:9090或板卡IP进入Jupyter Notebook。版本匹配是我首先想强调的。PYNQ官方镜像里集成了一些pl内核的PETALINUX驱动不同PYNQ版本对应不同Vivado版本。常见问题是Vivado版本太高、生成的bitstream/hwh格式与PYNQ环境不兼容导致Overlay加载失败。如果你不想折腾直接选“PYNQ v2.6 Vivado 2020.1”这个组合相对稳定。2.2 用模板建立第一个HLS工程打开Vivado HLS点Create New Project项目名随意工程位置不要带中文路径。关键的环节是Part Selection搜索并选择xc7z020clg400-1也就是PYNQ-Z2的芯片型号。选错芯片会导致综合结果和板卡资源不匹配所以这一步务必核对清楚。创建项目后Vivado HLS会生成一个顶层函数入口。新手最容易犯的错误是直接从头开始写代码其实可以直接用工具自带的模板入门菜单File - New File选择FIR、Matrix Multiplication或FFT示例先看一遍模板代码再改成自己的算法。这些模板的接口定义和pragma写法都是官方维护的直接拿来做试验省去查手册的时间。2.3 熟悉HLS的标准开发流程在Vivado HLS/Vitis HLS里HLS开发不是一锤子买卖而是分阶段反复迭代C仿真C Simulation在PC上编译并运行C代码验证算法功能正确。这个过程不涉及硬件只是把C代码当普通程序跑方便在早期暴露逻辑错误。C综合C Synthesis把C代码转换成RTL生成Verilog/VHDL以及对应的综合报告时序、资源、吞吐量预估。联合仿真C/RTL Co-Simulation把C测试平台转换到RTL仿真环境中运行验证综合出的硬件行为和C代码一致。导出IPExport RTL把综合结果封装成Vivado可以调用的IP核一般选择IP Catalog格式。我实际使用的建议是C仿真阶段多写几个用例至少覆盖正常输入、边界输入、矩阵尺寸变化的情况C综合报告出来后先看“Latency”延迟和“Trip Count”循环次数判断哪些循环是性能瓶颈联合仿真过了再导出否则导出后再改代码一轮要额外花不少时间。2.4 一个容易忽略的问题hwh文件是PYNQ的灵魂很多新手把bitstream烧进板子加载Overlay时报pynqpl.Warning: No hwh file found然后就懵了。PYNQ加载一个overlay依靠的不仅是bit文件还有hwh文件硬件握手文件。hwh文件本质是Vivado Block Design生成的硬件描述里面记录了IP实例名、寄存器地址段、中断号等关键信息。在Vivado正常生成比特流后开发机工程里可以找到project_name.gen/sources_1/bd/block_design/hw_handoff/block_design.hwh把它和.bit文件放到PYNQ板子上同一个overlay目录并保持同名前缀比如mmult.bit和mmult.hwh配对。没有hwhPYNQ无法知道IP挂载在哪个地址Python侧也就没法操作寄存器。这一点我放在环境搭建部分说是因为它决定了你后面几步能不能跑通。3. 手把手写一个矩阵乘法加速器HLS核心流程完整实操3.1 硬件接口怎么定m_axi与s_axilite的组合HLS设计IP时首先要思考的是“硬件怎么和外界通信”。矩阵乘法的场景是ARM侧把两个输入矩阵放在DDR里FPGA侧要从DDR读取数据、计算、再把结果写回DDR。最自然的接口方案是m_axi接口让HLS生成的IP作为AXI Master直接访问DDR地址主动读A、B矩阵写回C矩阵。s_axilite接口作为AXI Slave允许ARM通过一组控制寄存器启动IP、传入矩阵数据地址、读取状态标志。为什么不用AXI-StreamAXI-Stream适合流式数据比如视频像素流。矩阵乘法需要随机访问整个矩阵地址跳变明显用AXI-Stream会让主机端手动切分数据复杂度高。而m_axi像一个大水管可以按地址直接读DDR中任意位置配合突发传输burst对矩阵乘法这类数据块访问非常友好。顶层函数设计成void matrixmul(int *A, int *B, int *C, int size) { #pragma HLS INTERFACE m_axi portA offsetslave bundlegmem0 depth16384 #pragma HLS INTERFACE m_axi portB offsetslave bundlegmem1 depth16384 #pragma HLS INTERFACE m_axi portC offsetslave bundlegmem2 depth16384 #pragma HLS INTERFACE s_axilite portsize #pragma HLS INTERFACE s_axilite portreturn // 核心计算 }其中offsetslave表示基地址寄存器会暴露在s_axilite接口中ARM会通过寄存器来告诉IP“矩阵A的物理地址是多少”。depth用来提示工具这块内存的最大访问深度影响突发划分建议按矩阵最大元素数设置。3.2 第一版朴素矩阵乘法先验证功能再谈性能如果只想验证流程可以从最直接的实现开始void matrixmul(int *A, int *B, int *C, int size) { #pragma HLS INTERFACE m_axi portA offsetslave bundlegmem0 #pragma HLS INTERFACE m_axi portB offsetslave bundlegmem1 #pragma HLS INTERFACE m_axi portC offsetslave bundlegmem2 #pragma HLS INTERFACE s_axilite portsize #pragma HLS INTERFACE s_axilite portreturn for (int i 0; i size; i) { for (int j 0; j size; j) { int acc 0; for (int k 0; k size; k) { acc A[i * size k] * B[k * size j]; } C[i * size j] acc; } } }这段代码在C仿真阶段完全没问题综合也能通过。它的性能很差原因在于内存访问模式内层循环每读一个数都直接访问DDR地址随机性高AXI总线无法形成有效的突发传输大量时间浪费在数据搬运上。我在第一次实测时128x128的int矩阵乘法用这个版本跑硬件执行时间能到几百毫秒比ARM上直接用numpy还慢。这不代表HLS没用只说明算法映射时不能把PC上的内存模型直接搬到FPGA上——FPGA的强项是片上存储和并行计算而片上BRAM容量是有限的必须通过数据分块让数据尽量留在片内。3.3 性能优化的核心思路分块缓存和流水线并行要做出真正能加速的版本核心思想是“分块”把大矩阵切成适合BRAM容纳的子块先从DDR把子块搬到片上数组计算完再搬结果回DDR。这样整个计算过程大部分时间在片上BRAM里操作DDR只负责整块数据的起止搬移AXI burst效率大幅提升。分块矩阵乘法的骨架如下#define BLOCK 16 void matrixmul(int *A, int *B, int *C, int size) { #pragma HLS INTERFACE m_axi portA offsetslave bundlegmem0 depth16384 #pragma HLS INTERFACE m_axi portB offsetslave bundlegmem1 depth16384 #pragma HLS INTERFACE m_axi portC offsetslave bundlegmem2 depth16384 #pragma HLS INTERFACE s_axilite portsize #pragma HLS INTERFACE s_axilite portreturn int Ablock[BLOCK][BLOCK]; int Bblock[BLOCK][BLOCK]; #pragma HLS ARRAY_PARTITION variableAblock cyclic factor4 dim2 #pragma HLS ARRAY_PARTITION variableBblock cyclic factor4 dim1 for (int i0 0; i0 size; i0 BLOCK) { for (int j0 0; j0 size; j0 BLOCK) { int Cblock_acc[BLOCK][BLOCK] {0}; #pragma HLS ARRAY_PARTITION variableCblock_acc complete dim0 for (int k0 0; k0 size; k0 BLOCK) { // 从DDR批量搬运Ablock和Bblock load_block(A, Ablock, i0, k0, size); load_block(B, Bblock, k0, j0, size); // 计算两个BLOCKxBLOCK分块的乘累加 multiply_block(Ablock, Bblock, Cblock_acc, BLOCK); } // 写回C分块 store_block(C, Cblock_acc, i0, j0, size); } } }这里load_block、multiply_block、store_block的内部实现可以继续优化例如在multiply_block的最内层循环加#pragma HLS PIPELINE II1让乘累加每时钟周期能启动一次对Ablock和Bblock做ARRAY_PARTITION把同一行的多个元素放到不同BRAM端口上实现多路并行读取。当初我从朴素版改成分块版后128x128矩阵乘法从几百毫秒降到了几十毫秒量级再配合流水线和数组并行化最终可以压到十几毫秒甚至个位数毫秒取决于时钟频率和资源使用。3.4 C/RTL联合仿真别急着导出IPC综合通过后一定跑一次C/RTL联合仿真。联合仿真会把C测试平台的激励信号映射到RTL仿真器里验证生成的硬件逻辑和C代码行为一致。我遇到过不止一次C仿真全对一到联合仿真就报数据错误最后定位到是顶层函数里指针参数被当作局部缓存时综合出的接口握手时序和testbench预期不一致。解决办法是测试平台里尽量用ap_vld或ap_none等接口约束控制好ap_start/rst信号或者干脆检查m_axi的depth是否真的覆盖了访问范围depth设小了仿真时地址超出范围也会报错。联合仿真需要较长时间一开始不要跑太大矩阵16x16、32x32验证逻辑足够等到板上再跑128x128。这个习惯能省下大量调试时间。3.5 在Vivado里把IP接到Zynq上HLS导出IP后打开Vivado并创建Block Design按下列步骤连线添加ZYNQ7 Processing System IP运行Block Automation按PYNQ-Z2的板级配置启用UART、SD、USB和Ethernet。如果只是纯硬件加速验证可以不启用全部外设但DDR必须配好。添加导出的HLS IP运行Connection Automation让它自动连接AXI接口。设置PL时钟。PYNQ-Z2常见配置是FCLK_CLK0为100MHz或150MHzHLS IP的时钟默认会连到这个时钟。时钟频率越高性能越好但时序收敛压力也越大新手建议先100MHz跑通。地址分配。在Address Editor里给自定义IP分配地址段一般落在0x40000000~0x7FFFFFFF区域内比如0x40000000到0x4000FFFF。如果m_axi接口无法自动分配地址需要手动给DDR空间赋值。生成比特流时间取决于工程复杂度。执行Generate Bitstream后Vivado会顺带生成hwh文件。连线时有个小陷阱Zynq的PS侧默认只有S_AXI_GP0和S_AXI_GP1可以用来给PL侧IP发配置信息。如果你的HLS IP用了三个m_axi接口而且需要直接访问DDR最好再给自定义IP连接到S_AXI_HP0或HP1否则数据通路可能全部挤在GP口上带宽明显不足。这个细节会直接影响性能我在后面的调优章节再展开。4. 在PYNQ上用Python调起硬件加速从比特流到Jupyter4.1 组织overlay文件在PYNQ板卡上新建一个工作目录比如/home/xilinx/jupyter_notebooks/mmult/把Vivado生成的.bit和.hwh文件复制进去并确保两个文件的主名一致例如mmult.bitmmult.hwhJupyter里加载from pynq import Overlay overlay Overlay(mmult.bit) print(overlay)如果一切正常overlay会列出IP实例比如mmult_0。这一步如果报错找不到hwh先检查两个文件主名是否一致如果报PL版本不匹配多半是Vivado版本和PYNQ镜像不匹配。4.2 用allocate分配物理连续内存PYNQ里不能用普通的numpy数组直接给硬件IP传地址。普通数组的内存是虚拟内存物理地址可能不连续AXI Master无法正确访问。正确做法是使用pynq.allocate分配物理连续且地址对齐的缓冲区from pynq import allocate import numpy as np import time N 128 A allocate(shape(N, N), dtypenp.int32) B allocate(shape(N, N), dtypenp.int32) C allocate(shape(N, N), dtypenp.int32) A[:] np.random.randint(0, 10, (N, N)).astype(np.int32) B[:] np.random.randint(0, 10, (N, N)).astype(np.int32) # 如果缓冲区是cacheable写完后需要flush确保数据回到DDR A.flush() B.flush()allocate分配出来的缓冲区自带device_address属性后面会把这三个缓冲区的物理地址写入HLS IP的寄存器。4.3 寄存器操作启动IP并等待完成HLS生成的s_axilite接口寄存器布局是可以预测的。对于普通HLS IP0x00地址是控制寄存器bit0写1触发启动bit1是完成标志函数参数比如A、B、C指针和size按地址0x10、0x18、0x20、0x28顺序映射。每个参数是64位地址长度但低32位足够覆盖PYNQ-Z2的DDR物理地址空间。用Python操作寄存器的方式见下面代码# 假设overlay.mmult_0是HLS IP实例 ip overlay.mmult_0 # 写入三个缓冲区地址和size ip.write(0x10, A.device_address) ip.write(0x18, B.device_address) ip.write(0x20, C.device_address) ip.write(0x28, np.int32(N)) # 启动硬件 ip.write(0x00, 1) # 等待done标志CTRL寄存器bit1 while (ip.read(0x00) 0x2) ! 0x2: pass # 读取结果前如果缓冲区是cacheable需要invalidate C.invalidate() result C.copy()这段代码基本就是所有PYNQHLS IP通用模板。如果你发现C矩阵值全为0最先检查的是是否忘了写缓冲区地址其次检查size是否写成了0x28位宽不对再次检查DMA缓冲区是否flush。4.4 实测性能对比软件和硬件到底差多少我在PYNQ-Z2上跑的128x128 int矩阵乘法时钟100MHz软件侧直接用ARM A9上的numpy使用OpenBLAS大约40ms左右第一版朴素HLS IP反而要三百多毫秒优化后的分块HLS IP大约10~15ms。如果继续优化比如使用双缓冲、增大分块、提高PL时钟到150MHz可以进一步降到个位数毫秒。这个对比不是想强调硬件一定比软件快而是想说清楚一个道理FPGA加速的收益来自于“并行计算 数据重新调度”的组合缺一不可。如果只是把PC的循环直接翻译成硬件很可能比CPU还慢。5. 性能调优的关键路径从200ms优化到十几毫秒5.1 为什么第一版HLS IP比软件还慢很多新手在HLS第一步就跑出了“负优化”然后就下结论说HLS没用。实际上第一版慢主要有两个原因内存访问没有突发朴素版本里内层循环逐个访问DDR地址AXI总线无法利用burst模式相当于每次读一个32位数据都需要和DDR做一次完整握手延迟极大。计算没有并行即便内层循环没有流水线每个时钟周期也只能做一次乘累加硬件资源和电脑CPU相比没有任何优势。解决这两个问题的方向很明确先做数据分块让DDR访问变成连续的大块数据传输再做循环优化让FPGA上的DSP48E1和BRAM真正忙碌起来。5.2 HLS优化指令的实战组合PIPELINE、UNROLL、ARRAY_PARTITIONHLS代码的性能主要由三个基本操作决定PIPELINE让循环体在不同迭代之间重叠执行。最理想的是II1也就是每个时钟周期都能处理一次循环迭代。UNROLL把循环展开用多个计算单元同时处理多个数据。展开因子越大DSP资源使用越多吞吐越高。ARRAY_PARTITION把大数组切分成多个小数组分布在多个BRAM端口上解决“同时需要读多个数据”时的带宽瓶颈。矩阵乘法里我常用的组合是#pragma HLS ARRAY_PARTITION variableAblock cyclic factor4 dim2 #pragma HLS ARRAY_PARTITION variableBblock cyclic factor4 dim1 #pragma HLS PIPELINE II1这样设计后乘累加循环每周期可以同时从Ablock的不同列和Bblock的不同行读取多个数据配合DSP资源做并行乘法。资源约束也要心里有数。XC7Z020的PL侧有220个DSP48E1、140个BRAM36Kb。如果展开因子太大、数组切得太碎综合时资源超了就会自动降频或疯狂布线结果反而变慢。我的经验是小矩阵如128x128用factor4或8不用追求极端展开资源利用率保持在70%以内时序会宽松得多。5.3 dataflow与双缓冲解决数据搬运和计算互相等待分块设计里还有一个隐藏瓶颈搬运数据需要时间计算也需要时间如果搬一块、算一块、再搬一块、再算一块数据通路就是串行的。解决办法是用#pragma HLS DATAFLOW把多个循环块流水起来让上一个循环还在搬运时下一个循环已经开始计算更进阶的是用寄存器或BRAM做双缓冲一块buffer在计算时另一块buffer在接收DDR数据。DATAFLOW的坑在于不同循环之间如果有数据依赖工具会强制插入乒乓缓冲Ping-Pong Buffer资源占用会成倍增长。我当时调试时发现资源占用突然翻了一倍就是这个原因。最终我选择手动控制分块循环把搬运和计算拆到不同函数再用DATAFLOW串起来才在资源和性能之间找到平衡点。5.4 哪些算法适合放进HLS加速器做了几个HLS项目后我对“什么算法适合HLS加速”有了比较清晰的认识适合计算密集、数据复用率高、有规则访问模式的算法矩阵乘、卷积、FIR滤波、FFT都是典型。不太适合分支逻辑复杂、依赖递归、数据访问完全随机或需要频繁动态分配内存的算法。勉强适合图形图像预处理、视频缩放、颜色空间转换这类流式处理但要优先选AXI-Stream接口而不是m_axi效率会更高。如果你拿不准可以先在C语言阶段统计一下算法内层循环是否存在大量乘累加访问数组时地址是否规律如果两个条件都满足HLS大概率能帮你吃下这块硬件加速的蛋糕。6. 常见问题与排查技巧那些文档里不写的坑6.1 仿真过了但板子跑挂先怀疑复位和时钟HLS的C仿真过关不代表上板一定正常工作。最常见的情况是IP没收到有效复位或时钟没起来。PYNQ-Z2的PL侧时钟由PS输出如果Block Design里没配置好FCLK或没使能对应的GP端口IP内部状态机可能永远停在初始状态。排查办法在Jupyter里写一段循环读取IP CTRL寄存器观察ap_idle位是否为1。如果始终是0说明IP根本没有正确初始化回头查时钟和复位连接。另一个快速验证方案是在IP里加一个固定值输出寄存器比如某个内部计数器的低16位加载后直接读寄存器如果读到非零值就说明IP在跑。6.2 burst没有打满带宽上不去优化后的HLS IP在C综合报告里可能显示吞吐很高但实测性能依然拉胯。问题通常出在AXI burst效率上。打开Vivado的波形或HLS的接口报告看m_axi接口的burst长度是否达到十几以上。如果burst长度总被限制在4或8说明综合器认为访问的数据不连续或存在别名问题。一个很实用的技巧在函数参数里给指针加__restrict限定符告诉编译器A、B、C不会指向同一片内存综合器就能更大胆地安排连续读。另外检查depth参数是否够大过小的depth会让工具保守地限制突发。6.3 hwh文件缺失、地址冲突和DDR缓存一致性Overlay加载失败的第三个高频问题就是hwh文件缺失或IP实例名对不上。PYNQ加载hwh后会按照实例名生成Python属性。如果你的Block Design里IP名字叫matrixmul_0Python里就得用overlay.matrixmul_0如果名字叫mmult_0就用overlay.mmult_0。命名不统一虽然不影响硬件功能但代码里找起来很痛苦建议在建工程时就有意识地统一命名。数据错乱还可能是缓存一致性问题。PYNQ的allocate默认分配non-cacheable内存通常不需要手动flush。但如果你用其他途径分配内存或者对一些非默认target的缓冲区写回和失效操作就很重要。我习惯总是调用A.flush()和C.invalidate()代价是几个微秒换来的是调试时少掉头发。6.4 我的个人心得版本管理、增量优化和耐心最后分享几条我自己的实操经验纯属个人习惯但效果很好。工程目录做好版本管理。HLS代码、Vivado工程、PYNQ overlay目录三者分开每轮优化都记录基线比如“朴素版320ms”、“分块版45ms”这样能清晰看出改动带来了什么效果。优化要增量跑。不要一次把分块、流水线、数组切分、DATAFLOW全部堆上出问题根本定位不了。每加一条pragma就重新综合一次虽然慢一点但调试效率反而更高。对参数化代码先在C仿真里用小矩阵比如16x16验证所有边界再上板跑128x128。板上调试的成本远高于PC仿真能提前发现的问题就不要留到板子上。如果你也想在PYNQ-Z2上试一把自定义硬件加速建议就从矩阵乘法这个例子开始跑通全流程再把其中的负载替换成你真正关心的算法。整套“C代码 - HLS综合 - Vivado集成 - PYNQ Python调用”的链路一旦打通后续做图像卷积、FIR滤波、简单神经网络推理都会顺很多。