菜鳥一枚,最近也學習了關于 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)
…
未完待更。。。。