カテゴリー「PYNQ」の記事

Xilinx ZYNQ FPGAボード

PYNQ-Z1のOverlay読み込みとPythonからのFPGA PLの制御(2)

前回の「PYNQ-Z1のOverlay読み込みとPythonからのFPGA PLの制御」の続編です。前回のポストでは、Overlayのダウンロードに"/dev/xdevcfg"というデバイスファイルを叩いたり、LEDの点滅のためにmmapを直接叩いたりと非常に低レベルな処理になっていました。今回は、PYNQらしく、PythonのOverlayクラスを使ってOverlayをダウンロードし、LEDの制御もMyLEDクラスを使って、より抽象化した処理で動くようにしてみました。

Overlayクラスを使えるようにする

Overlayクラスを使ってOverlayをダウンロードするためには、カスタムOverlayのbitstreamを作った際のVivado tclファイルが必要になります。まず、カスタムOverlayのVivadoプロジェクトを開いて、Block Designを表示します。

MyLED BD

「File > Export > Block Design..」を実行すると、Block Designをtclファイルとしてexportしてくれます。ここでは、led.tclというファイル名で保存しました。

 BD export

生成した、tclファイルをPYNQのカスタムbitstreamと同じディレクトリーに保存します(/home/xilinx/pynq/bitstream)

2017 02 04 Overlay tcl

次に、以下のPyhonコードを実行すると、Overlayクラスのインスタンス作成性時に、led.tclがパースされて、bitstreamに含まれるI/Oとそのアドレスが抽出されます。この例では、axi_gpio_0が0x41200000で識別されていることが分かります。

Instansiate Ovlerlay

次に、axi_gpio_0に値を書き込むことによってLEDを制御するコードを書いて、/home/xilinx/pynq/boardにmyled.pyというファイル名で保存します。14行目のコードで、PL.ip_dictアトリビュートから、GPIOのアドレスを抽出して、mmioクラスのコンストのラクターに渡しています。これで、GPIOのアドレスの紐付けができました。

from pynq import MMIO
from pynq import PL

LEDS_OFFSET0 = 0

class MyLED(object):
    """This class controls the onboard LEDs vi axi_gpio_0. """
    _mmio = None
    _leds_value = 0

    def __init__(self):
        """Create a new MyLED object. """
        if MyLED._mmio is None:
            MyLED._mmio = MMIO(int(PL.ip_dict["SEG_axi_gpio_0_Reg"][0],16),16)
        MyLED._mmio.write(LEDS_OFFSET0, 0x0)

    def set(self, value):
        """Turn on a LED.
        
        Parameters
        ----------
        Value = GPIO out data
        
        Returns
        -------
        None
        
        """
        MyLED._mmio.write(LEDS_OFFSET0, value)

/home/xilinx/pynq/board/__init__.pyに最後の一行を追加しておきます。

from .led import LED
from .rgbled import RGBLED
from .switch import Switch
from .button import Button
from .myled import MyLED

 これでMyLEDクラスが使えるようになりました。

Jupyter Notebookの作成

以下のように、Overlayをダウンロードして、MyLEDクラスを使ってLEDを点灯させます。

Jupyter Notebook

 これで以下のように、カスタムOverlayを使ってLチカができます。前回のコードよりだいぶ簡潔で分かりやすなりました。

後書き

ということで、思ったより簡単にカスタムOverlayをPYNQのお作法に従って動かすことができました。PYNQにもZYBOのように安価なSDSoCのボード限定ライセンスが提供されればソフト屋さんもFPGAを使ったハードウェアオフロードが簡単にできるようになるのではと思います。PYNQのSDSoC、来ないのかな。

 

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配下のディレクトリーやファイルにアクセスが可能です。

QYNQ Folder

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を接続しました。

MyLED Block Design

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は以下の通りです。

Color LED notebook

RGB LEDのLチカが動きました。

次に、一旦動いているNotebookをshutdownします(これを行わずに、Overlayを再ダウンロードするとLinuxが固まる時があります)。

Stop RGB LED

次に、先ほど作ったbitsteamをOverlayとしてダウンロードし、LEDを点滅させるコードを書いてみました。bitstreamクラスやMMIOクラスの低レベル処理をそのまま使っています。

MyLED blink2

Runすると、LED0〜LED3が点滅しました!!

このコードでは、GPIO0のアドレス、0x41200000に対して直接書き込みを行うことによってLEDの点滅を行なっています。非常に低レベルな処理で記述していますが、本来はこれを上位のクラスでラッピングして使いやすいオブジェクトとして見せることになります(今回は、上位クラスの作成はサボっています)。

後書き

今回の実験の成果を3月4日のPYNQ祭りでLTします。Lチカだけではつまらないので、もう少し機能を追加した内容で発表できればと思っています(2月は仕事でドタバタしそうなので、どこまでできるかなのですが)。前段の普通の発表や、@cobac さんのLTと被りそうな気がしますがその際はご容赦を...

PYNQで動的にFPGAをコンフィグして使用することが可能であることが分かりました。これは結構画期的だと思います。Arduinoやmbedの様にオープンソース(ハード?)のOverlayがライブラリとして流通するようになると面白いと思います。

2023年6月
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30  
無料ブログはココログ