PYNQ学习·第一个IP核

基于 AXI4 接口的 Verilog IP 核开发教程

最近我开始学习PYNQ板和Vivado开发流程,与之前接触的Quartus开发流程不同,Vivado开发主要集中在IP核的设计和集成上,通过IP核的调用连接实现目标功能。在查阅资料的过程中,我发现大部分PYNQ的入门教程都使用HLS高级语言综合作为起步项目,而基于Verilog等RTL原语开发的教程较少。此外,一些教程已经过时,不再适用于当前的软件版本。因此,我写下这篇文章,记录我作为新手小白的学习经验。

本文教程适用于Vivado 2022.1,并在搭载v3.0.1镜像的PYNQ-Z1板上验证可用。

前期准备

关于PYNQ板的连接和启动,官网已有详细说明,这里不再赘述。

为在Vivado中导入PYNQ Z1开发板,需要先下载开发板信息文件,并将其放置在{Vivado Dir}\data\xhub\boards\XilinxBoardStore\boards\Xilinx目录下。

如果使用2021.2或更早版本的Vivado,板信息文件则存放在{Vivado Dir}\data\boards目录下。

创建IP模板

在Vivado工程中选择工具栏中的Tools->Create and Package New IP,在弹出的窗口中使用Create a new AXI4 Peripheral创建一个带有AXI4接口的IP核模板,并配置接口参数,这里以功能较为简单的AXI4 Lite接口作为演示,最后选择Edit IP打开生成的IP核工程,如果选择了其他选项,可以从Project Manager中的IP Catalog找到该工程,右键选择Edit in IP Packager打开该IP核。

image-20230313190958912

image-20230313190917622

模板工程中含有两个文件,其中project.v文件中定义了AXI总线的接口并例化了一个支持AXI总线通信功能的模块,project_AXI.v文件具体实现了AXI总线功能,为了快速入门,我们重点关注其实现用户逻辑功能的寄存器操作部分,其中,最核心的寄存器是四个带有slv前缀的寄存器(寄存器数量由之前接口设置中的Number of Registers决定)与输出数据的reg_data_out,模板实现的默认逻辑为:当向地址X写入数据时,数据会保存在对应的slv_regX中,而从地址X读出数据时,会读取到slv_regX中的数据;因此我们只需要对这些寄存器进行操作就可以借由模板中的方法实现通信的功能。

1
2
3
4
5
6
7
8
	...	
	reg [C_S_AXI_DATA_WIDTH-1:0]	slv_reg0;
	reg [C_S_AXI_DATA_WIDTH-1:0]	slv_reg1;
	reg [C_S_AXI_DATA_WIDTH-1:0]	slv_reg2;
	reg [C_S_AXI_DATA_WIDTH-1:0]	slv_reg3;
	...
	reg [C_S_AXI_DATA_WIDTH-1:0]	 reg_data_out;
	...

作为从机,在写逻辑中,会根据地址将数据存入对应的slv_reg寄存器,通过将switch case内slv_reg改为其他寄存器,可以改变写入寄存器的位置,不过在模板中slv_reg仅用于发送与接收,为避免错误,建议不要直接修改模板内容,而在其他时序逻辑中转存其值。另外注意这里的default语句理论上是不会运行的,因此更改此处的赋值左寄存器并不会将写入数据储存到新位置处。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	always @( posedge S_AXI_ACLK )
	begin
        if ( S_AXI_ARESETN == 1'b0 )...
	  else begin
	    if (slv_reg_wren)
	      begin
	        case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
	          2'h0:
	            for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
	              if ( S_AXI_WSTRB[byte_index] == 1 ) begin
	                slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
	              end
	          2'h1:...
	          2'h2:...
	          2'h3:...
	          default : begin
	                      slv_reg0 <= slv_reg0;
	                      slv_reg1 <= slv_reg1;
	                      slv_reg2 <= slv_reg2;
	                      slv_reg3 <= slv_reg3;
	                    end
	        endcase
	      end
	  end
	end    

而在读逻辑中,首先当地址位变动时,会将指定寄存器数据读入reg_data_out中,并在接下来的读时序中将reg_data_out内数据送出。因此,可以修改switch case内赋值语句右值来将我们需要的寄存器数据通过指定地址读出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
	assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
	always @(*)
	begin
	      case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
	        2'h0   : reg_data_out <= slv_reg0;
	        2'h1   : reg_data_out <= slv_reg1;
	        2'h2   : reg_data_out <= slv_reg2;
	        2'h3   : reg_data_out <= slv_reg3;
	        default : reg_data_out <= 0;
	      endcase
	end

	always @( posedge S_AXI_ACLK )
	begin
	  if ( S_AXI_ARESETN == 1'b0 )
	    begin
	      axi_rdata  <= 0;
	    end 
	  else
	    begin    
	      if (slv_reg_rden)
	        begin
	          axi_rdata <= reg_data_out;
	        end   
	    end
	end    

编写IP逻辑

这里我们简单实现一个计算两数平均值的模块,

1
2
3
4
5
	reg [31:0] average_reg;
	always @(posedge S_AXI_ACLK)
	begin
	   average_reg <= (slv_reg0 >> 1) + (slv_reg1 >> 1) + (slv_reg0 & slv_reg1 & 1) ;
	end

同时,将读逻辑中的2'h2 : reg_data_out <= slv_reg2;修改为2'h2 : reg_data_out <= average_reg;使计算结果可以通过访问0b1000地址读出。

编写完成,依次运行Synthesis、Implement,验证无误后,可以在Package IP页的Review and Package中选择Re-Package IP重新生成IP核。

image-20230313201034505

调用IP核

返回原工程,在Block Design中加入封装好的IP核和ZYNQ7 PS,自动连线,Vivado会帮我们添加时钟和AXI控制器。

image-20230313202403918

保存Design,在Sources中右键选择Create HDL Wrapper让Vivado自动生成一个含有设计例化的顶层文件,综合实现生成bit流。

image-20230313202121696

上板验证

为在开发板上调用该IP核,需要使用Vivado生成三个文件:

  1. .bit文件:该文件可视作可执行文件,在PYNQ板上通过Overlay导入,生成在{Project Dir}\{Project}.runs\impl_1\{Top File}.bit处;
  2. .hwh文件:硬件描述文件,包含有对PYNQ各模块的配置,默认生成在{Project Dir}\{Project}.gen/sources_1/bd/{Block Design}/hw_handoff/{Block Design}.hwh处;
  3. .tcl文件(可选):早期版本的硬件描述文件,可读性较.hwh文件可读性更高,需要在工具栏File->Export->Export Block Design生成。

随后将.bit文件与.hwh文件改为同一名称,移动到PYNQ板内同一目录下,PYNQ默认将Overlay保存在/home/xilinx/pynq/overlays目录下。

在早期版本中,需要将.bit文件与同名.tcl上传至PYNQ板,不需要.hwh文件。

PYNQ板中,PS层与PL层的交互通过内存映射实现,因此我们需要知到挂载IP核对应的内存基址,在.tcl文件中,找到内存映射部分,可以看到average_0/S00_AXI对应的基址为0x43C00000,掩码为0x0000FFFF

1
2
  # Create address segments
  assign_bd_address -offset 0x43C00000 -range 0x00010000 -target_address_space [get_bd_addr_spaces processing_system7_0/Data] [get_bd_addr_segs average_0/S00_AXI/S00_AXI_reg] -force

而在.hwh文件中,可以找到对应模块配置内的PARAMATERS部分,同样可以读出基址和地址范围:

1
2
3
4
5
6
7
8
<PARAMETERS>
        <PARAMETER NAME="C_S00_AXI_DATA_WIDTH" VALUE="32"/>
        <PARAMETER NAME="C_S00_AXI_ADDR_WIDTH" VALUE="4"/>
        <PARAMETER NAME="Component_Name" VALUE="design_1_average_0_11"/>
        <PARAMETER NAME="EDK_IPTYPE" VALUE="PERIPHERAL"/>
        <PARAMETER NAME="C_S00_AXI_BASEADDR" VALUE="0x43C00000"/>
        <PARAMETER NAME="C_S00_AXI_HIGHADDR" VALUE="0x43C0FFFF"/>
</PARAMETERS>

最后,在jupyter notebook中,通过Overlay加载.bit文件,通过MMIO读写调用我们的IP功能,演示效果如下:

为方便调用,我们可以为其封装一个接口,如:

1
2
3
4
5
def avg(a, b):
    mmio = MMIO(0x43c00000, 0x10000)
    mmio.write(0x0, a)
    mmio.write(0x4, b)
    return mmio.read(0x8)

image-20230314124949099

加载评论