PYNQ-Z1のOverlay読み込みとPythonからのFPGA PLの制御
PYNQ-Z1はZYBOとよく似たZYNQ SoCを使ったFPGAボードですが、Pythonを使ってLinuxからFPGAのリソースにアクセスできることが特徴です。PYNQではFPGAのConfiguration Data (bitsteam) をOverlayと呼んでおり、標準でPYNQのI/Oやビデオ関係の処理ができるOverlayが提供されているのですが、ドキュメントを読んでいると、カスタムOverlayも作成できるとあります。
カスタムOverlayを作ってそれを動的にダウンロードできれば、FPGAのReconfigurableな利点を生かして、必要に応じてI/Oピンの割り当てを変更したり、さらには必要に応じてハードで処理する機能(画像処理など)を組み込めるという、RaspberryPiなどのマイコンボードでは絶対に真似ねできない、高い拡張性とPythonによる容易なプログラミングができるということになります。
本当にカスタムOverlayが動的にダウンロードできるのか(Linuxを動かしながらFPGAを動的にconfig変更できるのか)Lチカで実験してみました。
OverlayダウンロードとPythonからの制御の仕組みを調べる
PYNQはSMBのサーバーとして動いているため、WindowsのFile ExplorerやmacOSのFinderで/home/xilinx配下のディレクトリーやファイルにアクセスが可能です。
pl.pyファイルの中にあるbitstreamクラスがOverlayのダウンロードに関係していそうです。ファイルの中身を以下に示します。
class Bitstream(PL): """This class instantiates a programmable logic bitstream. Attributes ---------- bitfile_name : str The absolute path of the bitstream. timestamp : str Timestamp when loading the bitstream. Format: year, month, day, hour, minute, second, microsecond """ def __init__(self, bitfile_name): """Return a new Bitstream object. Users can either specify an absolute path to the bitstream file (e.g. '/home/xilinx/src/pynq/bitstream/base.bit'), or only a relative path. (e.g. 'base.bit'). Note ---- self.bitstream always stores the absolute path of the bitstream. Parameters ---------- bitfile_name : str The bitstream absolute path or name as a string. """ super().__init__() if not isinstance(bitfile_name, str): raise TypeError("Bitstream name has to be a string.") if os.path.isfile(bitfile_name): self.bitfile_name = bitfile_name elif os.path.isfile(general_const.BS_SEARCH_PATH + bitfile_name): self.bitfile_name = general_const.BS_SEARCH_PATH + bitfile_name else: raise IOError('Bitstream file {} does not exist.'\ .format(bitfile_name)) self.timestamp = '' def download(self): """The method to download the bitstream onto PL. Note ---- The class variables held by the singleton PL will also be updated. Parameters ---------- None Returns ------- None """ # Compose bitfile name, open bitfile with open(self.bitfile_name, 'rb') as f: buf = f.read() # Set is_partial_bitfile device attribute to 0 with open(general_const.BS_IS_PARTIAL, 'w') as fd: fd.write('0') # Write bitfile to xdevcfg device with open(general_const.BS_XDEVCFG, 'wb') as f: f.write(buf) t = datetime.now() self.timestamp = "{}/{}/{} {}:{}:{} +{}".format(t.year,t.month,t.day,\ t.hour,t.minute,t.second,t.microsecond) # Update PL information PL._client_request() PL._bitfile_name = self.bitfile_name PL._timestamp = self.timestamp PL._ip_dict = {} PL._gpio_dict = {} PL._server_update()
64〜65行目でbitsteamのデーターを読み込んで、72〜73行目で「general_const.BS_XDEVCFG」ファイルに書き込んでいます。general_const.BS_XDEVCFGの実態は、"/dev/xdevcfg"というデバイスファイルです。どうやらこのデバイスファイルに書き込みを行うことによってOverlay (bitstream) のダウンロードができるようです。
FPGAの制御(FPGAのレジスタへの書き込み)はmmio.pyのMMIOクラスを使って、/dev/memデバイスファイル経由でメモリー空間にマップされたFPGAのレジスタにアクセスしているようです。Pythonのコードは以下になります。
class MMIO: """ This class exposes API for MMIO read and write. Attributes ---------- virt_base : int The address of the page for the MMIO base address. virt_offset : int The offset of the MMIO base address from the virt_base. base_addr : int The base address, not necessarily page aligned. length : int The length in bytes of the address range. debug : bool Turn on debug mode if it is True. mmap_file : file Underlying file object for MMIO mapping mem : mmap An mmap object created when mapping files to memory. array : numpy.ndarray A numpy view of the mapped range for efficient assignment """ def __init__(self, base_addr, length=4, debug=False): """Return a new MMIO object. Parameters ---------- base_addr : int The base address of the MMIO. length : int The length in bytes; default is 4. debug : bool Turn on debug mode if it is True; default is False. """ if base_addr < 0 or length < 0: raise ValueError("Negative offset or negative length.") euid = os.geteuid() if euid != 0: raise EnvironmentError('Root permissions required.') # Align the base address with the pages self.virt_base = base_addr & ~(mmap.PAGESIZE - 1) # Calculate base address offset w.r.t the base address self.virt_offset = base_addr - self.virt_base # Storing the base address and length self.base_addr = base_addr self.length = length self.debug = debug self._debug('MMIO(address, size) = ({0:x}, {1:x} bytes).', self.base_addr, self.length) # Open file and mmap self.mmap_file = os.open(general_const.MMIO_FILE_NAME, os.O_RDWR | os.O_SYNC) self.mem = mmap.mmap(self.mmap_file, (self.length + self.virt_offset), mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE, offset=self.virt_base) self.array = np.frombuffer(self.mem, np.uint32, length >> 2, self.virt_offset) def __del__(self): """Destructor to ensure mmap file is closed """ os.close(self.mmap_file) def read(self, offset=0, length=4): """The method to read data from MMIO. Parameters ---------- offset : int The read offset from the MMIO base address. length : int The length of the data in bytes. Returns ------- list A list of data read out from MMIO """ if not length == 4: raise ValueError("MMIO currently only supports 4-byte reads.") if offset < 0 or length < 0: raise ValueError("Negative offset or negative length.") idx = offset >> 2 if idx << 2 != offset: raise MemoryError('Read operation unaligned.') self._debug('Reading {0} bytes from offset {1:x}', length, offset) # Read data out return int(self.array[idx]) def write(self, offset, data): """The method to write data to MMIO. Parameters ---------- offset : int The write offset from the MMIO base address. data : int / bytes The integer(s) to be written into MMIO. Returns ------- None """ if offset < 0: raise ValueError("Negative offset.") idx = offset >> 2 if idx << 2 != offset: raise MemoryError('Write operation not aligned.') if type(data) is int: self._debug('Writing 4 bytes to offset {0:x}: {1:x}', offset, data) self.array[idx] = np.uint32(data) elif type(data) is bytes: length = len(data) num_words = length >> 2 if num_words << 2 != length: raise MemoryError('Need an integer number of words') buf = np.frombuffer(data, np.uint32, num_words, 0) self.array[offset:offset + num_words] = buf else: raise ValueError("Data type must be int or bytes.")
60行目でopenしている、general_const.MMIO_FILE_NAMEの実態が/dev/memで、63〜66行目でmmapを使ってFPGAのレジスタをマッピングしています。
試験用のbitstreamを作る
試験用に、ZYNQ PSとGPIOのみの最小構成のシステムを作り、GPIOにLED0〜LED3を接続しました。
ZYNQ PLは、Diligent社PYNQサイトのZynq Presetからダウンロードした”pynq_revC.tcl”を使ってPLの設定を行います(Apply Configuration..でtclファイルを指定する)。さらに、ZYNQの構成でMAXI-GP0ポートを有効にします。次にAXI-GPIOを起こして接続したものが上記のブロックデザインになります。
合成・インプリ・ビットストリームの生成を行い、bitstreamをled.bitというファイル名で、PYNQに書き込みます。
動作確認
まずは、標準のOverlayを使ってRGB LEDをLチカするPyhonコードを動かしてみます。Jupyter Notebookは以下の通りです。
RGB LEDのLチカが動きました。
次に、一旦動いているNotebookをshutdownします(これを行わずに、Overlayを再ダウンロードするとLinuxが固まる時があります)。
次に、先ほど作ったbitsteamをOverlayとしてダウンロードし、LEDを点滅させるコードを書いてみました。bitstreamクラスやMMIOクラスの低レベル処理をそのまま使っています。
Runすると、LED0〜LED3が点滅しました!!
このコードでは、GPIO0のアドレス、0x41200000に対して直接書き込みを行うことによってLEDの点滅を行なっています。非常に低レベルな処理で記述していますが、本来はこれを上位のクラスでラッピングして使いやすいオブジェクトとして見せることになります(今回は、上位クラスの作成はサボっています)。
後書き
今回の実験の成果を3月4日のPYNQ祭りでLTします。Lチカだけではつまらないので、もう少し機能を追加した内容で発表できればと思っています(2月は仕事でドタバタしそうなので、どこまでできるかなのですが)。前段の普通の発表や、@cobac さんのLTと被りそうな気がしますがその際はご容赦を...
PYNQで動的にFPGAをコンフィグして使用することが可能であることが分かりました。これは結構画期的だと思います。Arduinoやmbedの様にオープンソース(ハード?)のOverlayがライブラリとして流通するようになると面白いと思います。
最近のコメント