Pynq和Zynq SoC Tutorial

由于毕业论文打算进行FPGA加速器的设计,希望能够打通计算机系统栈从上到下的各个层次,因此本文将记录Ultra96-V2这款SoC的使用。

基本配置

Zynq SoC主要由processing system (PS)和programming logic (PL)两部分构成。

January 19, 2021 - Pynq & Zynq SoC Tutorial

Arm通常被认为时处理系统(processing system, PS),用于支持软件程序或操作系统;而FPGA相当于可编程逻辑(programming logic, PL),用来实现高速逻辑、算术和数据流子系统。

January 19, 2021 - Pynq & Zynq SoC Tutorial

其中的外设,也就是所谓的IP核(Intellectual Property),既可以从Xilinx的库中获得,又可从开源项目中得到,或者自己创造,最终集成起来形成系统设计。

事实上,Zynq的处理系统里并非只有ARM处理器,还有一组相关的处理资源,形成一个应用处理器单元(Application Processing Unit, APU)

January 19, 2021 - Pynq & Zynq SoC Tutorial

Ultra96 v2的基本配置如下,由于连接线没有提供,因此最好上淘宝购买【4.0x1.7mm 5V转12V升压线】和【USB转MicroUSB线】。

  • Delkin 16 GB microSD card + adapter
  • Micron 2 GB (512M x32) LPDDR4 Memory
  • Xilinx Zynq UltraScale+ MPSoC ZU3EG A484

    • Application Processor Unit (APU): Processor Core Quad-core ARM® Cortex™-A53 MPCore™ up to 1.5GHz

      • Memory w/ECC L1 Cache 32KB I / D per core, L2 Cache 1MB, on-chip Memory 256KB

    • Real-Time Processor Unit (RPU): Processor Core Dual-core ARM Cortex-R5 MPCore™ up to 600MHz

      • Memory w/ECC L1 Cache 32KB I / D per core, Tightly Coupled Memory 128KB per core

Pynq

Pynq (Python productivity for Xilinx Zynq)目的是让开发者更易对嵌入式系统进行编程,而不用采用综合工具,通过以下三点实现:

  • 可编程的逻辑电路都以硬件库(libraries)的形式呈现,称为覆盖(overlays)。软件工程师能够直接选择合适的覆盖来实现他们的应用,用API访问。尽管创造新的覆盖依然需要硬件知识,但是一旦完成,就可以被多次使用。
  • Pynq用Python作为嵌入式处理器和覆盖的编程语言。
  • Pynq开源,希望能够面向所有计算平台和操作系统,通过浏览器端(jupyter)实现。用Jupyter Notebook+局域网可建立起主机(host)和SoC之间的联系,方便编程与通信。

烧写SD卡

开机环境配置可参考Getting Started一文。

Pynq官网下载Ultra96v2的镜像文件,解压出.img,并写入原装的Micro SD卡。如果SD卡里本来有东西,则需要先按照如下方式格式化、清除分区并烧写:

  • 用Win+R打开cmd并输入diskpart
  • 输入list disk,查看所有磁盘,注意避免选错磁盘(16G SD卡显示应该为14G)
  • 依据大小选中对应磁盘select disk x
  • 输入clean删除所有分区
  • Windows下用win32diskimage写入镜像文件

启动

January 19, 2021 - Pynq & Zynq SoC Tutorial

  • 插入Micro-SD卡
  • 插入USB线
  • 插入电源线
  • 按下SW4按钮开机
  • USB串口会自动在电脑上安装驱动
  • 在电脑上访问 http://192.168.3.1:9090 即可登入jupyter notebook

    • 密码为xilinx
    • 其他操作与正常的notebook一致

Overlay

自定义Overlay需要用Vivado HLS设计生成IP核,并烧写入FPGA的片上逻辑。

使用HLS设计IP核

参照Pynq官方文档,设计简单的加法器,下面为函数头文件及对应的testbench。

// kernel.h

#ifndef __KERNEL_H__

#define __KERNEL_H__

#include <ap_int.h>

#include <ap_fixed.h>

#include <hls_stream.h>

typedefap_int<32>bit32;

typedefap_uint<32>ubit32;

voidtest(inta,intb,int&c);

#endif

// main.cpp

#include "kernel.h"

voidtest(inta,intb,int&c){

#pragma HLS INTERFACE ap_ctrl_none port=return

#pragma HLS INTERFACE s_axilite port=a

#pragma HLS INTERFACE s_axilite port=b

#pragma HLS INTERFACE s_axilite port=c

c=a+b;

}

// test.cpp

#include <iostream>

#include <cassert>

#include "kernel.h"

usingnamespacestd;

intmain(){

inta=3;

intb=2;

intc=0;

test(a,b,c);

assert(c==5);

cout<<"Pass!"<<endl;

}

同时将下面脚本放在同一目录下,调用vivado_hls -f run.tcl运行,会生成IP核。

(如果要执行cosim,可能需要设置一下编译器头文件,参见Vivado HLS in a Nutshell

#########

# run.tcl

#########

# Project name

set hls_prj out.prj

# Open/reset the project

open_project ${hls_prj}-reset

# Top function of the design is "top"

set_top test

# Add design and testbench files

add_files main.cpp

add_files -tb test.cpp

open_solution "solution1"

# Use Zynq device

# set_part {xczu3eg-sbva484-1-e}

set_part {xczu3eg-sbva484-1-i}# ultra96-v2

# Target clock period is 10ns

create_clock -period 10

# Directives

############################################

# Simulate the C++ design

csim_design -O

# Synthesize the design

csynth_design

# Co-simulate the design

#cosim_design

# Implement the design

export_design -format ip_catalog

exit

impl/misc/src/xtest_hw.h文件里可以查看各个端口的起始地址。

生成比特流

由于Vivado本身并不包含所有的FPGA板卡,因此需要先手动添加Ultra96-v2的描述文件,见此文此文,即下面两条指令。

git clone https://github.com/Avnet/bdf

sudo cp-r bdf/ultra96v2 /tools/Xilinx/Vivado/2020.1/data/boards/board_files

  1. 打开Vivado,创建新RTL project(暂不添加source),添加板卡为ultra96-v2
  2. 点击Create Block Design

    • 添加Zynq UltraScale+ MPSoC IP核
    • 在Tools-Settings-IP-Repository中添加IP核的自定义目录,将整个HLS project文件夹添加进去即可,会自动检测出对应的IP核
    • 在Block Design界面重新添加Test(因为上面HLS的top函数为test)的IP核,点击并更名为adder(记住名字,这在之后的Pynq中会用到)
    • 点击Run Block Automation和Run Connection Automation
    • 可能需要对Zynq的另一个时钟手动连线,与第一个时钟连在一起即可(或者再点一次Run Connection Automation)

  3. 右击Sources-Design Sources里的项目名,对该项目生成顶层HDL wrapper(会变成一个.v文件)
  4. 直接点Bitstream generation,会自动运行Synthesis和Implementation
  5. 将对应的.tcl.bit.hwh文件拷贝出来,注意这几个文件都是必须的,尽管在Pynq读入overlay时只需指定bitstream的地址,但是其会自动检查其他相关文件

    • 回到block design页面,菜单File-Export-Export Block Design,得到adder.tcl
    • 将生成的bitstream拷贝出来,vivado/adder.runs/impl_1/design_1_wrapper.bit
    • 将生成的.hwh文件拷贝出来,vivado/adder.srcs/sources_1/bd/design_1/hw_handoff/design_1.hwh

可以利用下面的Makefile将必要的文件拷贝到当前目录下。

PROJECT_NAME= adder

copy:

cp out.prj/solution1/impl/misc/drivers/test_v1_0/src/xtest_hw.h xtest_hw.h

cp vivado/$(PROJECT_NAME).srcs/sources_1/bd/$(PROJECT_NAME)/hw_handoff/$(PROJECT_NAME).hwh $(PROJECT_NAME).hwh

cp vivado/$(PROJECT_NAME).runs/impl_1/$(PROJECT_NAME)_wrapper.bit $(PROJECT_NAME).bit

cp vivado/$(PROJECT_NAME).tcl $(PROJECT_NAME).tcl

在Pynq中导入overlay

将上述文件通过jupyter拷贝入SoC,创建一个新的notebook,然后就可以测试overlay是否正常运作了。(下面Overlay的初始化只需用到.bit.hwh两个文件)

frompynqimportOverlay

overlay=Overlay("base.bit")

在notebook中使用overlay?可以得到overlay的详细信息,用overlay.register_map可以看到所有端口信息。

add_ip=overlay.add# get module

# 1st method

# add_ip.register_map

add_ip.register_map.a=3

add_ip.register_map.b=4

add_ip.register_map.c# 7

# 2nd method

# check the address in the HLS-generated Verilog code

add_ip.write(0x10,4)

add_ip.write(0x18,5)

add_ip.read(0x20)# 9

如无意外,上述代码就可以通过FPGA的硬件电路执行了。

进阶

AXI端口

January 19, 2021 - Pynq & Zynq SoC Tutorial

主机(Master, M)是控制总线并发起会话的,而从机(Slave, S)是做响应的。

向量传输

下面的程序将数组in_arr读入,加1后再放到out_arr中输出,使用MMIO (Memory-Mapped IO)的方式访问(PS与PL共用内存地址)。

注意HLS pragma的写法:

  • m_axi为主机AXI协议,可以传大量数据,但需要CPU进行控制。depth指明数据传输量,port指明变量名,offset=slave代表地址偏移量依次递增,bundle代表将哪些变量端口捆绑在一起,如果bundle名称相同,则说明这些变量共用同一个传输端口。
  • s_axilite为从机AXI轻量协议,通常只能传简单的数据

#include <ap_int.h>

#include <ap_fixed.h>

#include <hls_stream.h>

typedefap_int<32>bit32;

typedefap_uint<32>ubit32;

voidtest(bit32in_arr[10],bit32out_arr[10]){

#pragma HLS INTERFACE m_axi depth=10 port=in_arr offset=slave bundle=INPUT

#pragma HLS INTERFACE m_axi depth=10 port=out_arr offset=slave bundle=OUTPUT

#pragma HLS INTERFACE s_axilite register port=return bundle=CTRL

for(inti=0;i<10;++i){

#pragma HLS pipeline

out_arr[i]=in_arr[i]+1;

}

}

经过Vivado HLS进行编译后,导出IP核。前面这一步跟之前的类似,关键在于下面Vivado内进行的Block Design (BD)。(不得不说,这一部分的相关教程几乎没有,因此我也是看了不少FPGA的设计范例才最终摸索出来应该怎么进行设计。)

主要步骤如下:

  1. 搜索添加Zynq IP核(刚添加时只有下图中Zynq UltraScale+ MPSoC的模块)
  2. 添加HLS project路径后,搜索添加刚生成的HLS IP。由于我们的top函数为test,因此生成出来的模块就叫Test。然后点击该IP,左侧会出现Block Properties一栏,修改名称为test,这是后面Pynq导入时需要用到的名称。观察该IP核可以看到,我们在HLS里的输入端口INPUT被综合成了AXI端口m_axi_INPUT_r,而输出端口OUTPUT被综合成m_axi_OUTPUT_r,控制端口则是s_axi_CTRL
  3. 重要!)添加两个AXI Smart Connecter,命名为axi_smc_0axi_smc_1,并双击将slave端口数目改为1,作为与PS(Zynq)通信的中转模块,分别手动连线。在连线前还需双击Zynq IP,调出2个高性能通信端口S_AXI_HPx_FPDm_axi_INPUT_r连接第一个SMC的输入端,第一个SMC的输出端连接PS的S_AXI_HP0_FPD,同理第二个SMC。(这一步也是我一开始没有做的,导致有部分输出端口闲置,功能不完善)
  4. 手动将重要的线连接后即可交由自动化程序将剩余工作完成,点击Run Block Automation和Run Connection Automation,可能后者需要执行两次。
  5. 最终可以获得完整的模块设计图,如下参考。

January 19, 2021 - Pynq & Zynq SoC Tutorial

后段综合步骤与adder的设计相同,在此不再赘述。导出.hwh.tcl.bit文件,传到板上。

接下来是Pynq的部署,首先需要查看HLS生成的硬件端口偏移地址,参见xtest_hw.h

  • 0x00主要是控制信号,第0位用来控制整个模块的启动与终止,对应bd里PS的外围设备ps8_0_axi_periph中的M00_AXI及PL的s_axi_CTRL端口,也即说明了控制命令的流动。
  • 0x100x18则是两个数据输入输出端口,对应bd的m_axi_INPUT_rm_axi_OUTPUT_r,用于传输PS内存地址,也即PL执行时会到这两个地址去读写数据。

// ==============================================================

// Vivado(TM) HLS - High-Level Synthesis from C, C++ and SystemC v2020.1.1 (64-bit)

// Copyright 1986-2020 Xilinx, Inc. All Rights Reserved.

// ==============================================================

// CTRL

// 0x00 : Control signals

// bit 0 - ap_start (Read/Write/COH)

// bit 1 - ap_done (Read/COR)

// bit 2 - ap_idle (Read)

// bit 3 - ap_ready (Read)

// bit 7 - auto_restart (Read/Write)

// others - reserved

// 0x04 : Global Interrupt Enable Register

// bit 0 - Global Interrupt Enable (Read/Write)

// others - reserved

// 0x08 : IP Interrupt Enable Register (Read/Write)

// bit 0 - Channel 0 (ap_done)

// bit 1 - Channel 1 (ap_ready)

// others - reserved

// 0x0c : IP Interrupt Status Register (Read/TOW)

// bit 0 - Channel 0 (ap_done)

// bit 1 - Channel 1 (ap_ready)

// others - reserved

// 0x10 : Data signal of in_arr_V

// bit 31~0 - in_arr_V[31:0] (Read/Write)

// 0x14 : reserved

// 0x18 : Data signal of out_arr_V

// bit 31~0 - out_arr_V[31:0] (Read/Write)

// 0x1c : reserved

// (SC = Self Clear, COR = Clear on Read, TOW = Toggle on Write, COH = Clear on Handshake)

#define XTEST_CTRL_ADDR_AP_CTRL 0x00

#define XTEST_CTRL_ADDR_GIE 0x04

#define XTEST_CTRL_ADDR_IER 0x08

#define XTEST_CTRL_ADDR_ISR 0x0c

#define XTEST_CTRL_ADDR_IN_ARR_V_DATA 0x10

#define XTEST_CTRL_BITS_IN_ARR_V_DATA 32

#define XTEST_CTRL_ADDR_OUT_ARR_V_DATA 0x18

#define XTEST_CTRL_BITS_OUT_ARR_V_DATA 32

Python执行完整代码如下。注意之前的参考样例会使用Xlnk来分配内存地址,但是Pynq官方说这个特性将会在后面的版本中被废除,因此下面采用的是allocate方法。

frompynqimportallocate

frompynqimportOverlay

importnumpyasnp

importtime

# 1. Allocate memory on PS

in_arr=allocate(shape=(10,),dtype=np.int32)

out_arr=allocate(shape=(10,),dtype=np.int32)

# 2. Initialize numpy array

np_in=np.array([-3,-1,1,3,5,7,9,11,13,15],dtype=np.int32)

np_out=np.zeros((10,),dtype=np.int32)

# 3. Copy data to the allocated location

np.copyto(in_arr,np_in)

np.copyto(out_arr,np_out)

# 4. Load in overlay

overlay=Overlay("axi_test.bit")

print("Bitstream loaded")

ip=overlay.test# this name is that specified in block design

# 5. Tell PL the allocated address on PS

ip.write(0x10,in_arr.device_address)# check HLS generated file

ip.write(0x18,out_arr.device_address)

# As another method, using register_map to set address is also allowed

# 6. Start execution

ip.write(0x00,0x1)

isready=ip.read(0x00)

while(isready==1):# wait PL to finish

isready=ip.read(0x00)

# 7. Read results

np.copyto(np_out,out_arr)

# 8. Test final results

correct_res=np.array([-2,0,2,4,6,8,10,12,14,16],dtype=np.int32)

print(np.array_equal(np_out,correct_res))

最终输出结果为True就证明完整的Pynq流程跑通啦!

直接内存访问 (Direct Memory Access, DMA)

DMA相比起MMIO则是一种更加高效的内存访问方式,不需要CPU介入,但需要引入额外的IP核。

先在HLS头文件中定义数据结构如下(axis_t也包含在<ap_axi_sdata.h>头文件中,但亲测不太奏效,会导致DMA永远在等待),注意除了data,还需要有一个控制信号last,用于写入DMA时告诉DMA是否是最后一个元素。last信号会被HLS自动识别并综合为后端硬件可识别的代码。

structaxis_t{

ap_int<32>data;

ap_int<1>last;

};

同时可以添加模版函数,方便读写操作。hls::stream<hls_stream.h>中的模版,专门用来做流数据传输(虽然数组也可以做,但是在cosim阶段没有办法识别死锁等错误)。

template<classT>

inlinevoidaxis_read(hls::stream<axis_t>&in_arr,T*out_arr,std::size_tsize){

for(inti=0;i<size;++i){

#pragma HLS loop_tripcount max=10

axis_ttmp=in_arr.read();

out_arr[i]=tmp.data;

}

}

template<classT>

inlinevoidaxis_write(T*in_arr,hls::stream<axis_t>&out_arr,std::size_tsize){

for(inti=0;i<size;++i){

#pragma HLS loop_tripcount max=10

axis_ttmp;

tmp.data=in_arr[i];

if(i==size-1)

tmp.last=1;// be careful

else

tmp.last=0;

out_arr.write(tmp);

}

}

主函数如下,axis即为AXI流(stream)数据传输,注意hls::stream要通过传引用方式传递。

voidtest(hls::stream<axis_t>&in_stream,hls::stream<axis_t>&out_stream){

#pragma HLS INTERFACE axis port=in_stream bundle=INPUT

#pragma HLS INTERFACE axis port=out_stream bundle=OUTPUT

#pragma HLS INTERFACE s_axilite register port=return bundle=CTRL

intin_arr[10];

intout_arr[10];

axis_read<int>(in_stream,in_arr,10);

for(inti=0;i<10;++i){

#pragma HLS pipeline

out_arr[i]=in_arr[i]+1;

}

axis_write<int>(out_arr,out_stream,10);

}

然后就可以通过Vivado HLS综合并导出IP核,再从Vivado中导入进行block design。

January 19, 2021 - Pynq & Zynq SoC Tutorial

需要手动配置的几个点:

  • 把AXI DMA IP核中的Enable Scatter Gather Engine给去掉,即去除右端无用的端口显示
  • 每个AXI DMA IP有一进一出的读写端口,S_AXI_LITE为控制端口

    • S_AXIS_S2MM连接自定义IP核的输出,M_AXIS_MM2S连接自定义IP核的输入端
    • M_AXI_MM2SM_AXI_S2MM连接AXI SMC,再(自动)连入PS

  • 将Zynq的S_API_HP0_FPD调出来,用于DMA IP的连接

后端综合后,可按以下步骤在pynq内部署运行。

frompynqimportallocate

frompynqimportOverlay

importnumpyasnp

importtime

in_arr=allocate(shape=(10,),dtype=np.int32)

out_arr=allocate(shape=(10,),dtype=np.int32)

np_in=np.array([-3,-1,1,3,5,7,9,11,13,15],dtype=np.int32)

np_out=np.zeros((10,),dtype=np.int32)

np.copyto(in_arr,np_in)

np.copyto(out_arr,np_out)

overlay=Overlay("dma_test.bit")

print("Bitstream loaded")

ip=overlay.test_0

# dma data transfer

dma=overlay.axi_dma_0

dma.sendchannel.transfer(in_arr)

dma.sendchannel.wait()

print("Done sending data")

dma.recvchannel.transfer(out_arr)

ip.write(0x00,0x1)

isready=ip.read(0x00)

while(isready==1):

isready=ip.read(0x00)

dma.recvchannel.wait()

print("Done receiving data")

print(out_arr)

参考资料

参考范例

以上是 Pynq和Zynq SoC Tutorial 的全部内容, 来源链接: utcz.com/a/128963.html

回到顶部