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