🔥

KV260でPSからPL経由でLEDチカしてみる

2023/01/15に公開

はじめに

先日、KV260でSystemVerilogでLEDチカしてみるという記事を書き、ZynqMP の PL から LED をチカチカさせる手順を書きました。

今回は、PS と PL のやり取りを行うために、先日のプログラムを拡張して PS から PL 経由で LED をチカチカさせてみます。

なお、お行儀はあまり気にせずに、本質の例示を目的に、なるべく最短コースで低レイヤーを直接実装します。

PS 経由での LED チカチカ

前回の記事と同じ方法で、ブロックデザイナで PS の準備まで出来ている前提とします。

PS から AXI バスを引き出す

初期状態で M_AXI_HPM0_FPD が出ているので、これをそのまま引き出します。

M_AXI_HPM0_FPD_0 追加

下記のようになります。

M_AXI_HPM0_FPD_0

今回は一応、リセット信号も下記のように引き出しておきます。

reset

ちなみに引き出した M_AXI_HPM0_FPD が PS からどのようなメモリマップになっているかは、下記のように確認できます。

アドレス

0xa0000000 番地から割り当てられていることが分かりますね。

ちなみに引き出した M_AXI_HPM0_FPD は展開してみると、下記のようになります。
AXI仕様に基づく沢山の信号があることがわかります。

AXIバス

RTLを書く

今回も SystemVerilog で RTL を書いていきます。

例を簡単にするために、PS からは シングルアクセスしかしないという前提で、大胆に回路を省略して LEDチカ に必要な信号だけを使って記述していきます。

AXIバスは効率よく外部の SDRAM などに連続アドレスにまとめてデータを読み書きできる仕組みを備えているので規格としては複雑ですが、PL のレジスタを読み書きするだけであれば、ある程度簡略化できます。

(簡略化した AXI4-Lite という規格もあり、AXI4-Lite に変換する手もあるのですが、本サンプルではとにかくシンプルに済ませるために、このまま扱っています)。

top.sv
`timescale 1ns / 1ps

module top(
        output logic    [0:0]   led     // LED用に出力を 1bit 定義
    );

    // ブロックデザインから信号を引き出す
    logic           aresetn;
    logic           aclk;

    logic   [15:0]  awid;
    logic   [39:0]  awaddr;
    logic           awvalid;
    logic           awready;
    logic   [127:0] wdata;
    logic           wvalid;
    logic           wready;
    logic   [15:0]  bid;
    logic           bready;
    logic   [1:0]   bresp;
    logic           bvalid;
    logic   [15:0]  arid;
    logic   [39:0]  araddr;
    logic           arready;
    logic           arvalid;
    logic   [15:0]  rid;
    logic   [1:0]   rresp;
    logic           rlast;
    logic   [127:0] rdata;
    logic           rvalid;
    logic           rready;

    design_1
        i_design1
            (
                .pl_clk0_0                  (aclk       ),
                .pl_resetn0_0               (aresetn    ),

                .M_AXI_HPM0_FPD_0_awid      (awid       ),
                .M_AXI_HPM0_FPD_0_awaddr    (awaddr     ),
                .M_AXI_HPM0_FPD_0_awvalid   (awvalid    ),
                .M_AXI_HPM0_FPD_0_awready   (awready    ),
                .M_AXI_HPM0_FPD_0_wdata     (wdata      ),
                .M_AXI_HPM0_FPD_0_wvalid    (wvalid     ),
                .M_AXI_HPM0_FPD_0_wready    (wready     ),
                .M_AXI_HPM0_FPD_0_bid       (bid        ),
                .M_AXI_HPM0_FPD_0_bready    (bready     ),
                .M_AXI_HPM0_FPD_0_bresp     (bresp      ),
                .M_AXI_HPM0_FPD_0_bvalid    (bvalid     ),
                .M_AXI_HPM0_FPD_0_arid      (arid       ),
                .M_AXI_HPM0_FPD_0_araddr    (araddr     ),
                .M_AXI_HPM0_FPD_0_arready   (arready    ),
                .M_AXI_HPM0_FPD_0_arvalid   (arvalid    ),
                .M_AXI_HPM0_FPD_0_rid       (rid        ),
                .M_AXI_HPM0_FPD_0_rresp     (rresp      ),
                .M_AXI_HPM0_FPD_0_rlast     (rlast      ),
                .M_AXI_HPM0_FPD_0_rdata     (rdata      ),
                .M_AXI_HPM0_FPD_0_rvalid    (rvalid     ),
                .M_AXI_HPM0_FPD_0_rready    (rready     )
            );
    

    // AXI バスから LED を書き換え
    always_comb begin
        awready = (awvalid && wvalid);
        wready  = (awvalid && wvalid);
        arready = 1'b1;
    end

    always_ff @(posedge aclk) begin
        if ( ~aresetn ) begin  // AXIバスの論理は負論理
            // 初期化
            bid     <= 'x;
            bresp   <= 'x;
            bvalid  <= 1'b0;
            rid     <= 'x;
            rresp   <= 'x;
            rlast   <= 'x;
            rdata   <= 'x;      // 初期化不要なものは不定値にしておく
            rvalid  <= 1'b0;
            led     <= 0;
        end
        else begin
            // valid を出しているときに ready であれば受け付けられたとして valid を倒す
            if ( bready ) bvalid <= 1'b0;
            if ( rready ) rvalid <= 1'b0;

            // 書き込みの受付
            if ( awvalid && wvalid ) begin
                led    <= wdata[0];  // 書き込まれた値をLED値とする
                bid    <= awid;      // IDを返す
                bresp  <= '0;        // 正常完了
                bvalid <= 1'b1;
            end

            // 読み込みの受付
            if ( arvalid ) begin
                rid    <= arid; // IDを返す
                rresp  <= '0;   // 正常完了
                rlast  <= 1'b1; // 最後のデータであることを示す(シングルアクセスなら常に1)
                rdata  <= led;  // LEDの状態を返す
                rvalid <= 1'b1;
            end
        end
    end

endmodule

AXIバスの詳細は ARM社のページ から取得できます。Xilinx にも資料があります。

簡単に説明すると、valid/ready 方式のハンドシェークを行うチャネルが複数あり、ここでは PS がマスター、PL がスレーブとなります。

  • aw ではじまる書き込みアドレスを伝えるチャネル (マスター → スレーブ)
  • w ではじまる書き込みデータを伝えるチャネル(マスター → スレーブ)
  • b ではじまる書き込み完了を伝えるチャネル(スレーブ → マスター)
  • ar ではじまる読み込みアドレスを伝えるチャネル (マスター → スレーブ)
  • r ではじまる読み込みデータを伝えるチャネル (スレーブ → マスター)

の5つのチャネルがあり、それぞれで通信する必要があります。

valid と ready が同時に 1 になったサイクルでやり取りが成立します。送る側が valid を制御し、受け取る側が ready を制御しますが、常に受け取れる場合は ready は 常に 1 にしていても構いませんが、今回は、awvalid と wvalid の両方が揃うのを待ち合わせるために ready を使っています。

bitstream の生成

今回もトップネットだけですので、前回の記事と同様に論理合成と配置配線を実行して top.bit を作成して、KV260 にコピーしてください。

PS のソフト

今回は PS のソフトを C++ で書いてみたいと思います。

KV260 上の Ubuntu 22.04 には gcc などがありますので、PS のソフトは KV260 上でセルフコンパイルすることができます。

今回は /dev/mem を使って、0xa0000000 番地に直接アクセスしてみたいと思います。

適当なエディタで下記のようにソースコードを作成ください。

led_blinking.cpp
#include <iostream>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
    // デバイスオープン
    auto fd = open("/dev/mem", O_RDWR);
    if ( fd <= 0 ) {
        std::cout << "open error" << std::endl;
        return 1;
    }
    
    // メモリをマップ
    auto iomap = mmap(0, 0x10000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0xa0000000);
    if ( iomap == nullptr ) {
        std::cout << "mmap error" << std::endl;
        close(fd);
        return 1;
    }

    // LED点滅
    for ( int i = 0; i < 10; ++i ) {
        *(volatile unsigned char *)iomap ^= 1;
        usleep(500000);
    }

    // クローズ
    munmap(iomap, 0x10000);
    close(fd);

    return 0;
}

実行

準備が出来たらさっそく実行してみましょう。

Ubuntu のターミナルから、下記のように bitstream を PL に書き込んでください。

sudo sh -c "echo 0 > /sys/class/fpga_manager/fpga0/flags"
sudo mkdir -p /lib/firmware
sudo cp top.bit /lib/firmware/
sudo sh -c "echo top.bit > /sys/class/fpga_manager/fpga0/firmware"

続けて、C++ のソースをビルドして、sudo を使って root 権限で実行します。

g++ led_blinking.cpp
sudo ./a.out 

これで LED が点滅したかと思います。

!
kv260_led_blink

お疲れさまでした。

おわわりに

2回の記事で、少し ZynqMP の扱い方が見えてきたのではないでしょうか?

ここからさらに PL から DDR4-SDRAM の使い方が見えてくるとさらに利用用途が広がっていきます。

以前 Ultra96 用には udmabuf を試してみる (Ultra96V2編) という記事を書きました。

同じ動作をする、KV260版のプロジェクトは こちら にありますので、興味のある方はぜひお試しください。

  • Device Tree Overlay を使ったハードウェアの再構成
  • u-dma-buf を使った DDR4-SDRAM の領域確保と PS からのアクセス
  • 自作 DMA による PL からの DDR4-SDRAM のアクセス

などを試すことができます。

よき ZynqMP でのプログラミングライフを!

余談

今回のアクセス経路

今回 PS の APU から PL にアクセスした経路ですが、Zynq UltraScale+ MPSoC テクニカル リファレンス マニュアル (UG1085) から引用した図に赤矢印を付けてみました。

picture 7

もう少し詳しい図の方だと、下記のようになります。

picture 8

SoC の中で、広帯域な AXIバスにてダイレクトに PS と PL が接続されているのがわかりますね。

エディタについて

私は KV260 上でのファイルの編集を ssh経由で VS Code Remote Develop で行うことが多いです。

バス規格について

今回は AXI バスをそのまま使いましたが、ブロックデザイナを使わずに RTL でPLのレジスタを読み書きするだけであればいささか冗長に思います。
私はよく WISHBONEバス に変換してから利用しています。

ILAを使ってみる

今回説明しませんでしたが ILA を使って、実機の信号を覗くことも出来ます。
下記のような感じになります。是非使い方調べてみてください。

picture 6

追記 (2024/11/28)

記事とは細かい部分が少し異なりますが、概ね同じものをこちらに追加しております。

GitHubで編集を提案

Discussion