• Ultra96V2开发板简单使用


    概述

    最近向老师借了一块Ultra96 V2开发板学习FPGA。之前虽然也有接触过FPGA开发板,但第一次用的是只有一个FPGA核的Artix-7开发板,用的也是最传统的流程,即写好verilog模块,调一下网表文件,然后直接烧录到FPGA中;第二次用的是Zynq系的Pynq z2开发板了,但是也是用别人生成好的比特流文件。所以这是我第一次使用Zynq系的开发板编写硬件程序。这里记录一个简单的HLS程序从编写到生成再到板子上运行的过程。

    内容

    开发板配置

    比较简单,从这里下载对应的镜像,然后烧录到SD卡上。烧录的过程不一定要使用教程里推荐的软件,我就直接用平常制作系统盘的refus程序,经测试也可以。卡插上,连接Micro USB和电源就可以启动开发板。这个Micro USB可以充当网线的功能,所以开发板启动完可以在浏览器中输入192.168.3.1使用板子的Jupyter Notebook服务器,也可以直接ssh连接xilinx@192.168.3.1。

    简单端口的HLS程序运行

    HLS(High Level Synthesis,高层次综合)是Xilinx搞的将C/C++语言转换成Verilog/VHDL的技术,本文用HLS实现硬件程序。首先打开HLS IDE,创建项目,其中选择硬件的部分我选的是xqzu3eg-sfrc784-1-i。创建结束后添加代码(计算两个数的积):

    #include "product.h"
    
    void product(in_t a, in_t b, out_t &c) {
    
    #pragma HLS INTERFACE s_axilite register port=c
    #pragma HLS INTERFACE s_axilite register port=b
    #pragma HLS INTERFACE s_axilite register port=a
    #pragma HLS INTERFACE ap_ctrl_none port=return
    
    	c = a * b;
    }
    

    product.h包含了in_t和out_t的定义,这两个都是int类型。中间四行是给端口添加的修饰,前三行指定a、b和c为通过AXI总线控制的寄存器,在ZYNQ系的开发板中,PS端需要通过AXI总线读写寄存器来控制PL端的输入输出;第四行表示不生成product函数的返回值端口。注意用HLS编写的模块不能把模块输出作为函数的返回值,因为编译后如果生成返回值端口,在硬件模块正常运行的情况下返回值端口总是为0,我就被坑过一次,模块输出应该作为可变参数返回。

    这里我省略test bench的编写和C仿真的过程,直接点C Synthesis综合C代码,综合结束后点Export RTL生成IP核,后面在Vivado中使用。

    打开Vivado,创建项目,硬件的部分选择和HLS IDE里选的一样。创建结束后,在窗口左侧的Flow Navigator里点IP Catalog。在打开的IP Catalog标签里右键Vivado Repository,点Add Repository。这时可以选择刚才在HLS IDE里生成的IP核所在文件夹,一般是HLS项目文件夹/solution1/impl/ip,选择后在Vivado Repository上方就可以看到User Repository了,递归点开下拉箭头可以看到名为Product的IP核。

    接下来开始连接IP,在窗口左侧的Flow Navigator点Create Block Design。在中间的Diagram选项卡里右键空白处,点Add IP。在弹出的选择框里搜zynq,然后创建Zynq Ultra+ MPSoC,按此步骤再添加刚才自己创建的IP核Product(搜hls),这时上面会弹出一个Run Connection Automation的提示,点它再确认对话框,系统就会自动生成相应的连线:

    接着保存Block Design。点左侧的Generate Block Design生成各IP的组合,再点左侧的PROJECT MANAGER回到项目视图,在中间的Sources窗口右键刚才创建的Block Design文件(扩展名为bd),点Create HDL Wrapper生成IP组合模块的verilog文件。接着点左侧的Generate Bitstream生成比特流,如果提示还没Implementation就按提示先Implementation。

    接下来就是在板子上运行了。刚才编写的硬件程序是在PL端运行的,还需要PS端的软件程序启动和控制硬件程序。正常的开发流程是使用Vitis IDE编写C/C++程序,然后利用串口通信将比特流烧录到PL端,再将软件程序传输到存储卡中。不过UltraV2开发板支持PYNQ框架,这个框架能让用户使用Python方便地编写软件程序,还能直接启动存储卡上的比特流文件,不需要串口通信。所以这里介绍基于PYNQ的方法。

    首先将比特流文件和属性文件传到存储卡中,这一步可以用浏览器打开Jupyter Notebook页面上传,也可以用scp命令。比特流文件的路径一般是Vivado项目文件夹/项目名.runs/impl_1/BlockDesign名_wrapper.bit,属性文件的路径一般是Vivado项目文件夹/项目名.srcs/sources_1/bd/BlockDesign名/hw_handoff/BlockDesign名.hwh,两者需传到存储卡同个路径下,名字(不包括扩展名)也要改成一样,不然PYNQ无法识别。注意这个属性文件在老版本的PYNQ中需要的是tcl文件,新版本(本文使用的PYNQ版本为2.7.0)换成了hwh文件,所以可能比较老的教程和本文不同。

    然后开始写Python脚本:

    from pynq import Overlay
    
    # 读取比特流文件,假设其放在用户目录
    overlay = Overlay('/home/xilinx/design_1_wrapper.bit')
    # product_0是Block Design中用户定义的IP核的名字,见上图IP核上方黑色标题
    product = overlay.product_0
    # 利用AXI总线的功能,写入模块的输入端口寄存器
    product.register_map.a = 8
    product.register_map.b = 8
    # 利用AXI总线的功能,打印模块的输出端口寄存器的值
    print(product.register_map.c)
    

    输出为64,符合预期。

    可以输出product.register_map查看模块包含的寄存器,除了a、b和c还有一些控制寄存器。除了通过以上的方式读写寄存器,还可以通过地址的方式,查看HLS项目文件夹/solution1/impl/ip/drivers/模块名_v版本号/src/x模块名_hw.h

    // ==============================================================
    // Vivado(TM) HLS - High-Level Synthesis from C, C++ and SystemC v2019.2 (64-bit)
    // Copyright 1986-2019 Xilinx, Inc. All Rights Reserved.
    // ==============================================================
    // AXILiteS
    // 0x00 : reserved
    // 0x04 : reserved
    // 0x08 : reserved
    // 0x0c : reserved
    // 0x10 : Data signal of a
    //        bit 31~0 - a[31:0] (Read/Write)
    // 0x14 : reserved
    // 0x18 : Data signal of b
    //        bit 31~0 - b[31:0] (Read/Write)
    // 0x1c : reserved
    // 0x20 : Data signal of c
    //        bit 31~0 - c[31:0] (Read)
    // 0x24 : Control signal of c
    //        bit 0  - c_ap_vld (Read/COR)
    //        others - reserved
    // (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)
    
    #define XPRODUCT_AXILITES_ADDR_A_DATA 0x10
    #define XPRODUCT_AXILITES_BITS_A_DATA 32
    #define XPRODUCT_AXILITES_ADDR_B_DATA 0x18
    #define XPRODUCT_AXILITES_BITS_B_DATA 32
    #define XPRODUCT_AXILITES_ADDR_C_DATA 0x20
    #define XPRODUCT_AXILITES_BITS_C_DATA 32
    #define XPRODUCT_AXILITES_ADDR_C_CTRL 0x24
    

    里面给出了寄存器的映射方案,因此上面的Python代码后几行我们也可以这样写:

    product.write(0x10, 8)
    product.write(0x18, 8)
    print(product.read(0x20))
    

    BRAM端口的HLS程序运行

    有时我们需要将数组(的头指针)传给模块,由模块对数组的所有元素进行计算。这种情况下HLS会生成BRAM端口的硬件模块,将参与计算的值存在BRAM中,模块再从BRAM中取数。这里给出一个计算向量点积的HLS代码:

    #include "product.h"
    
    void product(in_t a[VSIZE], in_t b[VSIZE], out_t &c) {
    
    #pragma HLS INTERFACE s_axilite register port=c
    #pragma HLS INTERFACE s_axilite register port=b
    #pragma HLS INTERFACE s_axilite register port=a
    #pragma HLS INTERFACE ap_ctrl_none port=return
    
    	out_t r = 0;
    	for (int i = 0; i < VSIZE; i++) r += a[i] * b[i];
    	c = r;
    }
    

    VSIZE常量在product.h里定义了是16,其他和简单端口的HLS程序一样。按照前面的步骤生成IP核、创建Block Design并生成比特流。新的IP核和原来的端口一致,所以连线也没有变化。将比特流和属性文件放入存储卡,然后写Python脚本:

    from pynq import Overlay
    import numpy as np
    
    overlay = Overlay('/home/xilinx/design_1_wrapper.bit')
    product = overlay.product_0
    # 用numpy创建第一个向量数组,值为[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15]
    a = np.array(range(16), dtype=np.int32)
    # 用numpy创建第二个向量数组,值为[32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47]
    b = np.array(range(32, 48), dtype=np.int32)
    # 将数组a写入第一个BRAM端口中
    product.write(0x40, a.tobytes())
    # 将数组b写入第二个BRAM端口中
    product.write(0x80, b.tobytes())
    print(product.register_map.c)
    

    结果为5080,符合预期。

    可以看到AXI总线也支持通过地址读写BRAM,查看HLS项目文件夹/solution1/impl/ip/drivers/模块名_v版本号/src/x模块名_hw.h

    // ==============================================================
    // Vivado(TM) HLS - High-Level Synthesis from C, C++ and SystemC v2019.2 (64-bit)
    // Copyright 1986-2019 Xilinx, Inc. All Rights Reserved.
    // ==============================================================
    // AXILiteS
    // 0x00 : reserved
    // 0x04 : reserved
    // 0x08 : reserved
    // 0x0c : reserved
    // 0xc0 : Data signal of c
    //        bit 31~0 - c[31:0] (Read)
    // 0xc4 : Control signal of c
    //        bit 0  - c_ap_vld (Read/COR)
    //        others - reserved
    // 0x40 ~
    // 0x7f : Memory 'a' (16 * 32b)
    //        Word n : bit [31:0] - a[n]
    // 0x80 ~
    // 0xbf : Memory 'b' (16 * 32b)
    //        Word n : bit [31:0] - b[n]
    // (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)
    
    #define XPRODUCT_AXILITES_ADDR_C_DATA 0xc0
    #define XPRODUCT_AXILITES_BITS_C_DATA 32
    #define XPRODUCT_AXILITES_ADDR_C_CTRL 0xc4
    #define XPRODUCT_AXILITES_ADDR_A_BASE 0x40
    #define XPRODUCT_AXILITES_ADDR_A_HIGH 0x7f
    #define XPRODUCT_AXILITES_WIDTH_A     32
    #define XPRODUCT_AXILITES_DEPTH_A     16
    #define XPRODUCT_AXILITES_ADDR_B_BASE 0x80
    #define XPRODUCT_AXILITES_ADDR_B_HIGH 0xbf
    #define XPRODUCT_AXILITES_WIDTH_B     32
    #define XPRODUCT_AXILITES_DEPTH_B     16
    

    可以看到a和b都有对应的base、high、width和depth常量,其中base表示BRAM在总线中映射的首地址,high表示末地址,width和depth则是BRAM的位宽和深度。write方法除了可以往地址里写数,还可以往地址里写字节串,所以用Numpy数组的tobytes方法将数组转换成字节串就可以写入BRAM中了。

    DRAM端口的HLS程序运行

    BRAM是FPGA片上的存储器,速度快但是容量低,当用户需要从PS端往PL端传输大量数据时就力不从心了。解决方法是使用PS端和PL端共享的DRAM,也就是内存,把需要用到的大量数据传到内存中,再由硬件按需读取,这时模块的端口就是DRAM端口,硬件会将端口寄存器里的值解释为DRAM地址然后从DRAM中读数。

    HLS代码如下:

    #include "product.h"
    
    void product(in_t a[VSIZE], in_t b[VSIZE], out_t &c) {
    
    #pragma HLS INTERFACE m_axi depth=512 port=b
    #pragma HLS INTERFACE m_axi depth=512 port=a
    #pragma HLS INTERFACE s_axilite register port=c
    #pragma HLS INTERFACE s_axilite register port=b
    #pragma HLS INTERFACE s_axilite register port=a
    #pragma HLS INTERFACE ap_ctrl_none port=return
    
    	out_t r = 0;
    	for (int i = 0; i < VSIZE; i++) r += a[i] * b[i];
    	c = r;
    }
    

    和前面的代码基本一致,但多了两个端口修饰,将a和b同时声明为m_axi类型,表示这两个端口是DRAM端口,寄存器的值是DRAM地址,计算时要从DRAM中取数。depth是取数的位数,数组大小为16,int是32位,16乘32结果为512。

    继续按照前面的步骤生成IP核、创建Block Design,这时会发现product核的右侧多了一个m_axi_gmem的端口,需要经过总线连到DRAM。首先需要给ZYNQ核添加DRAM端口,办法是双击Zynq核对IP进行定制,在对话框左侧选择PS-PL Configuration,右边依次展开PS-PL Interfaces、Slave Interfaces、AXI HP,勾选AXI HPC0 FPD,然后点OK。这时会发现ZYNQ核多了一个S_AXI_HPC0_FPD端口(和一个时钟端口),再点Run Connection Automation,就可以自动连接了:

    相比前面多了一个AXI SmartConnect将product核和Zynq多出来的总线端口相连。继续按前面的步骤一直到生成比特流并传输文件。用来控制的Python脚本:

    from pynq import Overlay, allocate
    import numpy as np
    
    overlay = Overlay('/home/xilinx/design_1_wrapper.bit')
    product = overlay.product_0
    a = np.array(range(16), dtype=np.int32)
    b = np.array(range(32, 48), dtype=np.int32)
    # 创建连续的内存区域,供PL端读取
    mem_a = allocate((16, ), dtype=np.int32)
    mem_b = allocate((16, ), dtype=np.int32)
    # 将numpy数组中的元素复制到刚才创建的内存区域中
    np.copyto(mem_a, a)
    np.copyto(mem_b, b)
    # 获取刚才创建的内存区域的地址
    addr_a = mem_a.device_address
    addr_b = mem_b.device_address
    # 将地址传给product的端口寄存器
    product.register_map.a = addr_a
    product.register_map.b = addr_b
    print(product.register_map.c)
    

    结果为5080,符合预期。

    注意Numpy直接创建的数组不一定是连续的内存区域,也很难获得物理地址,所以pynq提供了一个继承自Numpy数组的缓冲区类型,既能被PL端读取,又能很方便地和Numpy之间互相传数据。注意老版本分配缓冲区的做法是先实例化一个名为Xlnk的类,然后用这个类分配缓冲区,新版本(本文使用的PYNQ版本为2.7.0)修改了API。

    总结

    目前网上关于这方面的教程良莠不齐,要么是只教到怎么装系统的,要么是用Vitis IDE走串口烧录的,要么只涉及简单端口的传输,要么针对的是常见的Zynq7000开发板,我也参考结合了很多资料才成功跑通并总结出本文,尤其是DRAM端口的相关内容。不过网上设计DRAM的一般使用的是流式传输,数据的传输是异步的,这种方法效率更高,适合传输大量数据,在Block Design的时候需要加上DMA核并连接中断相关端口,后面有时间再研究这种方法。

    如果使用HDL语言编写硬件模块流程也应该类似,Vivado应该有往HDL模块自动加上AXI总线协议并打包成IP核的功能,这点我也未尝试,后面有时间再研究。

  • 相关阅读:
    [LeetCode]Interleaving String关于遍历和动态规划
    [LeetCode]Power
    [LeetCode]Two Sum
    [LeetCode]wildcard matching通配符实现之贪心法
    [LeetCode]wildcard matching通配符实现之动态规划
    [LeetCode]wildcard matching通配符实现之递归
    回文分割
    iOS开发之应用内检测手机锁屏,解锁状态
    iOS 拨打电话
    iOS 中文转拼音
  • 原文地址:https://www.cnblogs.com/YuanZiming/p/15855435.html
Copyright © 2020-2023  润新知