基於 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 端程序。
在這裏插入圖片描述
可能有人要問,爲什麼綠色的箭頭有5條,而棕色有2條?其實這就是 AXI 總線的內部機制,下圖記錄了 AXI 讀寫的時序圖。可以很清楚地看到讀數據時,只需要兩個通道即可,而寫數據則需要三條通道,所以說對於訪存請求處理的兩個模塊共需要5條導線。而寄存器堆其實也是需要3條線,只是圖中省去了,因爲在 pynq 裏我們可以很方便的對寄存器地址進行判斷(4的倍數)。
在這裏插入圖片描述

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) 

未完待更。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章