基于 PYNQ 的 AXI 总线主从控制编写(ddr3的读写)

菜鸟一枚,最近也学习了关于 AXI 总线的相关知识。基于 PYNQ 编写了一个简单的 AXI 主从控制(牵涉到 DDR3 的读写)。

设计目的

设计出以下一个通过 AXI 总线连接的简单片上系统。包含以下部分:

PL 端逻辑

PL 端实现四个模块包括:

  • 寄存器堆
    寄存器堆主要是对指令等控制信息进行片上存储。具体的指令将由 PS 端写入。对于寄存器堆来说,其相当于 slave,而 PS 端的 jupyter 服务器相当于 master。使用的总线是 AXI Lite 总线棕色的大箭头)。

  • 控制端
    控制端主要做的是将寄存器堆送来的指令进行译码,进而从寄存器堆中取得各种配置信息,产生控制信号(主要是对 DRAM 的访存操作,相当于是 SDRAM 控制器)。

寄存器堆与控制端要进行连线。

  • 内存读处理(read)
    该模块主要负责处理对DRAM的读请求与接收到数据的处理。主要负责实现 AXI FULL 总线的相关逻辑。内部有一个读请求的 FIFO 队列。

  • 内存写处理(write)
    该模块主要负责处理对DRAM的写请求与写数据的处理。主要负责实现 AXI FULL 总线的相关逻辑。内部有一个写数据请求的FIFO队列。

控制端与读写请求处理模块(DRAM)是 master 与 slave 的关系。即控制端通过产生相应的访存请求信号,进而令读写请求模块进行访存的相应操作。简而言之,读写请求模块其实就是 AXI 总线的访存接口,其内部实现了 AXI 总线相应的握手机制,我们可以通过这两个模块在片上进行 ddr 的读写控制。故读写请求模块要挂在 AXI 总线上(绿色大箭头)。

下图中红色的框即为上面所说的 PL 端逻辑模块的系统框图。我们可以用 HDL 把模块实现好之后封装成 IP 核,写好输入输出端口,就可以挂在 AXI_Interconect 和 AXI register slice IP 核上并且连接到 ZYNQ 硬核上。进而编写 PS 端程序。
基于 PYNQ 的 AXI 总线主从控制编写(ddr3的读写)_第1张图片
可能有人要问,为什么绿色的箭头有5条,而棕色有2条?其实这就是 AXI 总线的内部机制,下图记录了 AXI 读写的时序图。可以很清楚地看到读数据时,只需要两个通道即可,而写数据则需要三条通道,所以说对于访存请求处理的两个模块共需要5条导线。而寄存器堆其实也是需要3条线,只是图中省去了,因为在 pynq 里我们可以很方便的对寄存器地址进行判断(4的倍数)。
基于 PYNQ 的 AXI 总线主从控制编写(ddr3的读写)_第2张图片

PS 端程序设计

接下来是软件方面的设计。我们知道,为了发挥 zynq 系列的优势,我们将通讯密集型的应用放在 CPU 上——即指令相关的控制信息的写入或预先对 ddr 数据的写入。

由于本篇博文是基于 pynq 的,故不牵涉到 sdk 编程(其实两者原理相同)。

  • 对 DDR 的读写
    由于我们使用的是 pynq,其中已经封装好了我们需要用到的相应 API,只需调用即可!在这一步里我们主要调用以下 API:
from pynq import Xlnk
xlnk.cma_array(shape=(),dtype=np.float16)

该函数返回一个 numpy 数组,相当于在 DRAM 里申请了一段大小为 shape 的空间,然后可以自由地对这一段空间进行读写操作。

  • 对寄存器堆的写入
    我们在 jupyter 服务器下可以很方便的对寄存器堆进行读写!只需提前定义好相应的寄存器地址即可。在这一步里我们主要调用以下 API:
from pynq import MMIO
ctrl = MMIO(0x40000000, 0x1000)# 开辟一块区域可供用户读写,主要是对寄存器(slave)的读写
ctrl.write(reg_addr, data) # 写寄存器
ctrl.read(reg_addr) # 读寄存器

我们封装一个 FPGA 类,将上面的操作完整写下来:

from pynq import Xlnk
from pynq import MMIO

# 寄存器地址
RESET_REG           = 0
PHY_ADDR_REG        = 4
START_REG           = 8
DONE_REG            = 12

class FPGA(object):
    initdone = False
    memalloc = False
    xlnk = None
    ctrl = None
    mem = None
    im_wh_q = None
    bb_list = list()
    cmdidx = 0
    
    def __init__(self, xlnk):
        self.xlnk = xlnk
        self.ctrl = MMIO(0x40000000, 0x1000) # 开辟一块区域可供用户读写,主要是对寄存器(slave)的读写
        self.im_wh_q = deque()
        try:
            self.mem = xlnk.cma_array(shape=(0x800000,4), dtype=np.float16) #分配buff内存区域,返回的是numpy数组
            self.memalloc = True           
        except:
            print("CMA array allocation failed (64MB)")
            print("Please call xlnk.xlnk_reset() or Restart notebook")
            xlnk.xlnk_reset()   
        
    def configure(self, weight_file, padding_file):
        if (self.memalloc == False):
            return False
        
        self.ctrl.write(PHY_ADDR_REG, self.mem.physical_address)  # 将内存偏移(起始地址)写入寄存器中
        self.ctrl.write(RESET_REG, 1)
        self.ctrl.write(RESET_REG, 0)
        rd = self.ctrl.read(PHY_ADDR_REG) # 读寄存器,看是否成功写入信息
        if (rd != self.mem.physical_address):
            xlnk.xlnk_reset()
            print("Error: Memory mapped IO failed")
            return False
        
        if (os.path.isfile(weight_file)):  # 打开一个本地的文件
            W = np.load(weight_file)
            woff = 0
			# 写权重到DRAM里面去
            for i in range(len(W)):
                self.mem[woff:(woff + W[i].shape[0]),:] = W[i]
                woff += W[i].shape[0]
        else:
            print("Error: Weight file not found")
            self.xlnk.xlnk_reset()
            return False
        # 写padding项到DRAM里面去
        if (os.path.isfile(padding_file)):
            W = np.load(padding_file)
            membase = 7 << 20  # 写pading项的起始地址,这个地址是如何得到的??? 7340032
			# 11100000000000000000000
            self.mem[membase: (membase | W.shape[0]), :] = W
        else:
            print("Error: Padding file not found")
            self.xlnk.xlnk_reset()
            return False
        
        membase = 4 << 20  # 4194304
		# 10000000000000000000000
        self.mem[membase:(membase | 106496), 3] = np.zeros((106496), dtype=np.float16) 

未完待更。。。。

你可能感兴趣的:(FPGA硬件设计)