プロジェクト

PYNQ-Z2(またはZybo Z7)で作る初めてのカスタムAXIペリフェラル

4つのレジスタを持つAXI4-Liteスレーブを手書きし、Zynqのブロックデザインに組み込んでハードウェアをエクスポートし、PYNQ上のPythonとVitisのベアメタルCの両方から駆動します。

上級AMD更新 2026年11月13日
ボードTUL PYNQ-Z2
所要時間約6時間
言語verilog
追加部品TUL PYNQ-Z2またはDigilent Zybo Z7-20、PYNQイメージを書き込んだmicroSDカード(Pythonルート用)、Micro-USBケーブル、PYNQ用のEthernetケーブルまたはWi-Fiドングル、VivadoとVitisを入れたコンピュータ

作るもの

Zynq-7000ではARMコアとプログラマブルロジックがAXIで通信しています。このバスを理解する最も速い方法は、IPウィザードをクリックして回ることではなく、自分でスレーブを書くことです。本プロジェクトでは、32ビットレジスタを4つ持つAXI4-Liteペリフェラルを作ります。制御レジスタが2つ、ステータスレジスタが1つ、そして毎クロック加算されるカウンタが1つです。最初の制御レジスタの4ビットはボードのLEDを駆動するので、書き込みが届いたことを目で確認できます。このペリフェラルをVivadoのブロックデザインでZynqプロセッシングシステムの隣に置き、アドレスを割り当て、2通りの方法で駆動します。PYNQ-Z2上のPythonセッションからMMIOを使う方法と、VitisのベアメタルCプログラムから使う方法です。Zybo Z7-20は同じデバイスを搭載しており、LEDの制約を変えるだけで動作します。違いが出る箇所には注記を入れています。

これが上級プロジェクトである理由は、Verilogが長いからではありません(約120行です)。ハンドシェイクの規則を正しく守る必要があること、そしてフローがRTLからブロックデザイン、ハードウェアのエクスポート、ソフトウェアへとまたがることにあります。ステートマシンに慣れていて、RTLからボードへを少なくとも一度は通しておくことをお勧めします。

必要なもの

  • TUL PYNQ-Z2またはDigilent Zybo Z7-20(どちらもZynq XC7Z020-1CLG400C)。より安価なZybo Z7-10でも、パートをxc7z010clg400-1に変えれば動作します。
  • VivadoとVitisの無償エディション。本稿執筆時点では2025.1が最新です。お使いのボードのボードファイルをインストールしておくと、プロセッシングシステムのプリセット(DDR、クロック、MIO)が自動で適用されます。
  • Pythonルートの場合: PYNQイメージを書き込んだmicroSDカード、ボードへのネットワーク接続、ブラウザまたはSSH。
  • Cルートの場合: JTAGとUARTを兼ねるmicro-USBケーブル。

設計

レジスタ

オフセット 名前 アクセス 内容
0x00 ctrl0 読み書き ビット0がカウンタイネーブル、ビット1がカウンタクリア(自己クリア)、ビット7:4がLED
0x04 ctrl1 読み書き 比較値
0x08 status 読み出し専用 ビット31:16が識別子0x1A2B、ビット1がカウンタのctrl1到達(クリアまでスティッキー)、ビット0がイネーブル
0x0C counter 読み出し専用 イネーブル中は毎クロック加算

ステータスレジスタの識別子は、身につけておく価値のある習慣です。ソフトウェアが最初に行うのはこの値の読み出しで、値が違っていれば、他のことに時間を浪費する前にアドレスマップがずれていると分かります。

5つのチャネルで構成されるAXI4-Lite

AXI4-Liteには独立した5つのチャネルがあり、それぞれ送り手のvalidと受け手のreadyを持ちます。転送は両方がハイになっているクロックエッジで起こります。書き込み側は3つのチャネルを使います。書き込みアドレス(AW)、書き込みデータ(W)、書き込み応答(B)です。読み出し側は2つで、読み出しアドレス(AR)と読み出しデータ(R)です。つまずきやすい規則は2つあります。validはreadyを待ってはならないこと(マスターはvalidをアサートし、転送が完了するまで保持します)、そしてスレーブはreadyをvalidより前にアサートしても後にアサートしてもよいが、ループを生むような形でreadyをvalidに組み合わせ的に依存させてはならないことです。

AXI4-Liteの書き込み。マスターがアドレスとデータを同時に提示し、スレーブが同じエッジで両方を受け取ってレジスタに書き込み、Bチャネルで応答します。clkawvalidawaddr0x00awreadywvalidwdata0xA1wreadybvalidbreadyctrl0old0xA1
AXI4-Liteの書き込み。マスターがアドレスとデータを同時に提示し、スレーブが同じエッジで両方を受け取ってレジスタに書き込み、Bチャネルで応答します。

ここで作るスレーブは、アドレスまたはデータをまだ保持していなければいつでも受け取り、それぞれをレジスタに覚えておき、両方がそろった時点で書き込みを実行します。これで、アドレスを先に送るマスター、データを先に送るマスター、両方を同時に送るマスターのすべてに対応できます。Zynqの汎用ポートは両方を同時に送りますが、テストベンチではもう一方の順序も試します。応答チャネルは書き込み後にbvalidを上げ、マスターがbreadyで受け取ったら下げます。

読み出しはもっと単純です。スレーブはデータを提示中でなければいつでもアドレスを受け取り、選択した値をrvalidをハイにしてrdataにレジスタ出力し、rreadyで受け取られたらrvalidを下げます。デコードするのはアドレスのビット3:2だけです。アドレスがこのスレーブの範囲内にあることはインターコネクトが既に確認しているので、上位ビットはここでは冗長です。

システム全体

プロセッシングシステム(PS)Cortex-A9x2, 650 MHzDDR3512 MBUART, SD,Ethernet, USBM_AXI_GP0AXI3マスターFCLK_CLK0 100 MHz, FCLK_RESET0_Nプログラマブルロジック(PL)インターコネクトAXI3 to AXI4-Liteaxi_lite_regs0x4000_0000LEDsproc_sys_reset同期化したrst_nclk, rst_n
Zynqのプロセッシングシステムは、汎用AXIマスターポートと、Vivadoが自動で追加するインターコネクトを経由してカスタムスレーブに到達します。

ZynqのGPポートが話すのはAXI3です。Vivadoの接続オートメーションは、GPポートとAXI4-Liteスレーブの間にインターコネクト(またはプロトコルコンバータ)を挿入し、さらにPSのリセットをFCLK_CLK0ドメインのクリーンな同期・アクティブローのリセットに変換するproc_sys_resetブロックを追加します。Verilogのclkポートとrst_nポートに付けた属性が、どのバスに属するかをVivadoに伝えるので、オートメーションが正しく配線してくれます。

コード

スレーブ

// Hand-written AXI4-Lite slave with four 32-bit registers.
//   0x00  ctrl0   RW  bit 0 = counter enable, bit 1 = counter clear (self-clearing), bits 7:4 = LEDs
//   0x04  ctrl1   RW  compare value
//   0x08  status  RO  bits 31:16 = 0x1A2B id, bit 1 = counter reached ctrl1 (sticky), bit 0 = enable
//   0x0C  counter RO  increments every clock while enabled
module axi_lite_regs (
    (* X_INTERFACE_INFO = "xilinx.com:signal:clock:1.0 clk CLK" *)
    (* X_INTERFACE_PARAMETER = "ASSOCIATED_BUSIF s_axi, ASSOCIATED_RESET rst_n" *)
    input  wire        clk,
    (* X_INTERFACE_INFO = "xilinx.com:signal:reset:1.0 rst_n RST" *)
    (* X_INTERFACE_PARAMETER = "POLARITY ACTIVE_LOW" *)
    input  wire        rst_n,

    // Write address channel. Only bits 3:2 are decoded here; the interconnect handles the rest.
    /* verilator lint_off UNUSEDSIGNAL */
    input  wire [11:0] s_axi_awaddr,
    /* verilator lint_on UNUSEDSIGNAL */
    input  wire        s_axi_awvalid,
    output wire        s_axi_awready,
    // Write data channel
    input  wire [31:0] s_axi_wdata,
    input  wire [3:0]  s_axi_wstrb,
    input  wire        s_axi_wvalid,
    output wire        s_axi_wready,
    // Write response channel
    output wire [1:0]  s_axi_bresp,
    output reg         s_axi_bvalid,
    input  wire        s_axi_bready,
    // Read address channel
    /* verilator lint_off UNUSEDSIGNAL */
    input  wire [11:0] s_axi_araddr,
    /* verilator lint_on UNUSEDSIGNAL */
    input  wire        s_axi_arvalid,
    output wire        s_axi_arready,
    // Read data channel
    output reg  [31:0] s_axi_rdata,
    output wire [1:0]  s_axi_rresp,
    output reg         s_axi_rvalid,
    input  wire        s_axi_rready,

    output wire [3:0]  led
);
    wire [1:0] waddr_sel = s_axi_awaddr[3:2];
    wire [1:0] raddr_sel = s_axi_araddr[3:2];

    reg [31:0] ctrl0, ctrl1, counter;
    reg        reached;

    wire enable = ctrl0[0];
    wire clear  = ctrl0[1];
    assign led  = ctrl0[7:4];

    wire [31:0] status = {16'h1A2B, 14'd0, reached, enable};

    // ---------------- Write path ----------------
    // Address and data may arrive in either order. Hold each until both are present.
    reg        aw_held, w_held;
    reg [1:0]  waddr_q;
    reg [31:0] wdata_q;
    reg [3:0]  wstrb_q;

    assign s_axi_awready = ~aw_held;
    assign s_axi_wready  = ~w_held;
    assign s_axi_bresp   = 2'b00;   // OKAY

    wire do_write = aw_held & w_held & (~s_axi_bvalid | s_axi_bready);

    // Merge new bytes into an existing word according to the strobes.
    function [31:0] merge(input [31:0] old, input [31:0] new_data, input [3:0] strb);
        integer b;
        begin
            merge = old;
            for (b = 0; b < 4; b = b + 1)
                if (strb[b]) merge[8*b +: 8] = new_data[8*b +: 8];
        end
    endfunction

    always @(posedge clk) begin
        if (!rst_n) begin
            aw_held      <= 1'b0;
            w_held       <= 1'b0;
            waddr_q      <= 2'd0;
            wdata_q      <= 32'd0;
            wstrb_q      <= 4'd0;
            s_axi_bvalid <= 1'b0;
            ctrl0        <= 32'd0;
            ctrl1        <= 32'd0;
        end else begin
            if (s_axi_awvalid & s_axi_awready) begin
                aw_held <= 1'b1;
                waddr_q <= waddr_sel;
            end
            if (s_axi_wvalid & s_axi_wready) begin
                w_held  <= 1'b1;
                wdata_q <= s_axi_wdata;
                wstrb_q <= s_axi_wstrb;
            end
            if (s_axi_bvalid & s_axi_bready) s_axi_bvalid <= 1'b0;

            // The clear bit lasts one clock.
            ctrl0[1] <= 1'b0;

            if (do_write) begin
                case (waddr_q)
                    2'd0: ctrl0 <= merge(ctrl0, wdata_q, wstrb_q);
                    2'd1: ctrl1 <= merge(ctrl1, wdata_q, wstrb_q);
                    default: ;   // status and counter are read-only, writes are accepted and ignored
                endcase
                aw_held      <= 1'b0;
                w_held       <= 1'b0;
                s_axi_bvalid <= 1'b1;
            end
        end
    end

    // ---------------- Counter and status ----------------
    always @(posedge clk) begin
        if (!rst_n || clear) begin
            counter <= 32'd0;
            reached <= 1'b0;
        end else begin
            if (enable) begin
                counter <= counter + 32'd1;
                if (counter == ctrl1) reached <= 1'b1;
            end
        end
    end

    // ---------------- Read path ----------------
    assign s_axi_arready = ~s_axi_rvalid;
    assign s_axi_rresp   = 2'b00;

    always @(posedge clk) begin
        if (!rst_n) begin
            s_axi_rvalid <= 1'b0;
            s_axi_rdata  <= 32'd0;
        end else begin
            if (s_axi_rvalid & s_axi_rready) s_axi_rvalid <= 1'b0;
            if (s_axi_arvalid & s_axi_arready) begin
                s_axi_rvalid <= 1'b1;
                case (raddr_sel)
                    2'd0: s_axi_rdata <= ctrl0;
                    2'd1: s_axi_rdata <= ctrl1;
                    2'd2: s_axi_rdata <= status;
                    default: s_axi_rdata <= counter;
                endcase
            end
        end
    end
endmodule

テストベンチ

テストベンチは、タスクとして書いた最小限のAXIマスターです。リセット値、リードバック、バイトストローブ、データがアドレスより先に来る順序、カウンタが動作して停止すること、クリアビットが自己クリアすること、比較フラグがセットされること、読み出し専用レジスタへの書き込みが受理されて無視されることを確認します。

`timescale 1ns / 1ps
// Drives the register block the way a processor would and checks every register.
module tb_axi_lite;
    reg clk = 1'b0;
    reg rst_n = 1'b0;
    always #5 clk = ~clk;   // 100 MHz, like FCLK_CLK0 on the Zynq

    reg  [11:0] awaddr = 0;  reg awvalid = 0;  wire awready;
    reg  [31:0] wdata = 0;   reg [3:0] wstrb = 0; reg wvalid = 0; wire wready;
    wire [1:0]  bresp;       wire bvalid;      reg  bready = 0;
    reg  [11:0] araddr = 0;  reg arvalid = 0;  wire arready;
    wire [31:0] rdata;       wire [1:0] rresp; wire rvalid; reg rready = 0;
    wire [3:0]  led;

    axi_lite_regs dut (
        .clk(clk), .rst_n(rst_n),
        .s_axi_awaddr(awaddr), .s_axi_awvalid(awvalid), .s_axi_awready(awready),
        .s_axi_wdata(wdata), .s_axi_wstrb(wstrb), .s_axi_wvalid(wvalid), .s_axi_wready(wready),
        .s_axi_bresp(bresp), .s_axi_bvalid(bvalid), .s_axi_bready(bready),
        .s_axi_araddr(araddr), .s_axi_arvalid(arvalid), .s_axi_arready(arready),
        .s_axi_rdata(rdata), .s_axi_rresp(rresp), .s_axi_rvalid(rvalid), .s_axi_rready(rready),
        .led(led)
    );

    integer errors = 0;

    // Write with address and data presented together, as the Zynq GP port does.
    task axi_write(input [11:0] addr, input [31:0] data, input [3:0] strb);
        begin
            @(posedge clk);
            awaddr <= addr; awvalid <= 1'b1;
            wdata  <= data; wstrb <= strb; wvalid <= 1'b1;
            bready <= 1'b1;
            fork
                begin @(posedge clk); while (!awready) @(posedge clk); awvalid <= 1'b0; end
                begin @(posedge clk); while (!wready)  @(posedge clk); wvalid  <= 1'b0; end
            join
            while (!bvalid) @(posedge clk);
            @(posedge clk);
            bready <= 1'b0;
            if (bresp !== 2'b00) begin errors = errors + 1; $display("FAIL: bresp %b", bresp); end
        end
    endtask

    // Write with the data channel first and the address a few clocks later.
    task axi_write_data_first(input [11:0] addr, input [31:0] data);
        begin
            @(posedge clk);
            wdata <= data; wstrb <= 4'hF; wvalid <= 1'b1; bready <= 1'b1;
            @(posedge clk); while (!wready) @(posedge clk); wvalid <= 1'b0;
            repeat (3) @(posedge clk);
            awaddr <= addr; awvalid <= 1'b1;
            @(posedge clk); while (!awready) @(posedge clk); awvalid <= 1'b0;
            while (!bvalid) @(posedge clk);
            @(posedge clk);
            bready <= 1'b0;
        end
    endtask

    task axi_read(input [11:0] addr, output [31:0] data);
        begin
            @(posedge clk);
            araddr <= addr; arvalid <= 1'b1; rready <= 1'b1;
            @(posedge clk); while (!arready) @(posedge clk); arvalid <= 1'b0;
            while (!rvalid) @(posedge clk);
            data = rdata;
            @(posedge clk);
            rready <= 1'b0;
        end
    endtask

    task check(input [255:0] name, input [31:0] got, input [31:0] want);
        begin
            if (got !== want) begin
                errors = errors + 1;
                $display("FAIL: %0s = 0x%08h, expected 0x%08h", name, got, want);
            end else begin
                $display("ok:   %0s = 0x%08h", name, got);
            end
        end
    endtask

    reg [31:0] v, v2;

    initial begin
        $dumpfile("tb_axi_lite.vcd");
        $dumpvars(0, tb_axi_lite);
        repeat (4) @(posedge clk);
        rst_n = 1'b1;
        repeat (2) @(posedge clk);

        axi_read(12'h008, v);
        check("status after reset", v, 32'h1A2B_0000);

        axi_write(12'h004, 32'hDEAD_BEEF, 4'hF);
        axi_read(12'h004, v);
        check("ctrl1 readback", v, 32'hDEAD_BEEF);

        axi_write(12'h004, 32'h0000_0042, 4'b0001);   // low byte only
        axi_read(12'h004, v);
        check("ctrl1 byte strobe", v, 32'hDEAD_BE42);

        axi_write_data_first(12'h000, 32'h0000_00A1);  // LEDs = 1010, enable
        axi_read(12'h000, v);
        check("ctrl0 readback", v, 32'h0000_00A1);
        check("led pins", {28'd0, led}, 32'h0000_000A);

        axi_read(12'h00C, v);
        axi_read(12'h00C, v2);
        if (v2 > v) $display("ok:   counter advanced from %0d to %0d", v, v2);
        else begin errors = errors + 1; $display("FAIL: counter did not advance (%0d, %0d)", v, v2); end

        axi_write(12'h000, 32'h0000_00A0, 4'hF);       // disable
        axi_read(12'h00C, v);
        axi_read(12'h00C, v2);
        check("counter frozen", v2, v);

        axi_write(12'h004, 32'h0000_0010, 4'hF);       // compare = 16
        axi_write(12'h000, 32'h0000_0003, 4'hF);       // clear then enable
        repeat (40) @(posedge clk);
        axi_read(12'h008, v);
        check("status reached", v, 32'h1A2B_0003);
        axi_read(12'h000, v);
        check("clear bit self-cleared", v, 32'h0000_0001);

        axi_write(12'h00C, 32'hFFFF_FFFF, 4'hF);       // write to read-only register is ignored
        axi_read(12'h008, v);
        check("status unchanged by RO write", v, 32'h1A2B_0003);

        if (errors == 0) $display("PASS: all AXI-Lite checks passed");
        else $display("FAIL: %0d errors", errors);
        $finish;
    end
endmodule

LEDの制約

## PYNQ-Z2 user LEDs LD0..LD3 (LVCMOS33)
set_property -dict { PACKAGE_PIN R14 IOSTANDARD LVCMOS33 } [get_ports {led[0]}]
set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports {led[1]}]
set_property -dict { PACKAGE_PIN N16 IOSTANDARD LVCMOS33 } [get_ports {led[2]}]
set_property -dict { PACKAGE_PIN M14 IOSTANDARD LVCMOS33 } [get_ports {led[3]}]

## Zybo Z7 alternative: LD0..LD3 are M14, M15, G14, D18

ビルドと書き込み

シミュレーション

iverilog -g2012 -o axi.vvp axi_lite_regs.v tb_axi_lite.v
vvp axi.vvp
verilator --lint-only -Wall axi_lite_regs.v

ok:の行が10行、PASS: all AXI-Lite checks passed、そしてVerilatorの警告なしの結果が出るはずです。

TclでVivadoのブロックデザインを作る

このスクリプトは、プロジェクトを作成し、ボードプリセット付きでZynq PSをインスタンス化し、Verilogをモジュールリファレンスとして追加し、接続オートメーションを実行し、アドレスを割り当て、ビットストリームを生成してハードウェアプラットフォームをエクスポートします。Zybo Z7の場合はboardとパートを変更し、LEDの制約を差し替えてください。vivado -mode batch -source build_bd.tclで実行します。

# Build the Zynq block design around the hand-written slave.
# Run: vivado -mode batch -source build_bd.tcl
set proj_dir ./regs_proj
set board    "tul.com.tw:pynq-z2:part0:1.0"      ;# Zybo Z7-20: digilentinc.com:zybo-z7-20:part0:1.1
set part     xc7z020clg400-1

create_project regs $proj_dir -part $part -force
set_property board_part $board [current_project]
add_files {axi_lite_regs.v}
add_files -fileset constrs_1 {pynq_z2.xdc}

create_bd_design design_1
create_bd_cell -type ip -vlnv xilinx.com:ip:processing_system7:5.5 ps
apply_bd_automation -rule xilinx.com:bd_rule:processing_system7 \
    -config {make_external "FIXED_IO, DDR" apply_board_preset "1"} [get_bd_cells ps]
set_property -dict [list CONFIG.PCW_USE_M_AXI_GP0 {1} CONFIG.PCW_FCLK0_PERIPHERAL_CLKSRC {IO PLL} \
    CONFIG.PCW_FPGA0_PERIPHERAL_FREQMHZ {100}] [get_bd_cells ps]

# Our RTL becomes a block design cell. Vivado infers the s_axi interface from the port names.
create_bd_cell -type module -reference axi_lite_regs regs
make_bd_pins_external [get_bd_pins regs/led]
set_property name led [get_bd_ports led_0]

apply_bd_automation -rule xilinx.com:bd_rule:axi4 \
    -config {Master "/ps/M_AXI_GP0" Clk "Auto"} [get_bd_intf_pins regs/s_axi]

# Give the slave a fixed address so the software side can hard-code it.
assign_bd_address -offset 0x40000000 -range 64K [get_bd_addr_segs {regs/s_axi/reg0}]

validate_bd_design
save_bd_design
make_wrapper -files [get_files design_1.bd] -top -import
set_property top design_1_wrapper [current_fileset]

launch_runs impl_1 -to_step write_bitstream -jobs 4
wait_on_run impl_1
write_hw_platform -fixed -include_bit -force regs.xsa

GUIでVivadoのブロックデザインを作る

  1. Create Projectで、RTL projectを選び、Boardsからお使いのボードを選択します。
  2. Add Sourcesでaxi_lite_regs.vを追加します。Add ConstraintsでXDCを追加します。
  3. Create Block Designを実行します。Add IPでZYNQ7 Processing Systemを追加します。Run Block Automationをクリックして既定値を受け入れると、ボードプリセットが適用されます。
  4. PSをダブルクリックし、PS-PL ConfigurationでM AXI GP0 interfaceが有効になっていることを確認します。Clock ConfigurationでFCLK_CLK0が100 MHzであることを確認します。
  5. キャンバスを右クリックしてAdd Moduleを選び、axi_lite_regsを選択します。ポート名がAXIの命名パターンに従っているので、Vivadoは単一のs_axiバスポートとして表示します。
  6. Run Connection Automationを実行し、s_axiにチェックを入れて受け入れます。インターコネクトとリセットブロックが現れます。
  7. ledピンを右クリックしてMake Externalを選びます。XDCに合わせてポート名をledに変更します。
  8. Address Editorで、スレーブのオフセットを0x4000_0000、範囲を64 Kに設定します。
  9. Validate Designを実行し、Sourcesでブロックデザインを右クリックしてCreate HDL Wrapperを選び、Vivadoに管理させます。
  10. Generate Bitstreamを実行します。その後、File、Export、Export Hardwareでビットストリームを含めてエクスポートすると、XSAが書き出されます。

ファイルの場所

PYNQにはビットストリームとハードウェアハンドオフファイルが、名前をそろえた状態で必要です。ビルド後に次を実行します。

cp regs_proj/regs.runs/impl_1/design_1_wrapper.bit regs.bit
cp regs_proj/regs.gen/sources_1/bd/design_1/hw_handoff/design_1.hwh regs.hwh
scp regs.bit regs.hwh xilinx@pynq:/home/xilinx/regs/

Vitisに必要なのはregs.xsaだけです。

テスト

PYNQのPythonから

PYNQカードからボードを起動し、Jupyterノートブックを開くかSSHで接続して(ユーザーxilinx、パスワードxilinx)、次を実行します。

from pynq import Overlay, MMIO
import time

ol = Overlay('/home/xilinx/regs/regs.bit')   # regs.hwh must sit beside it
print(ol.ip_dict.keys())                       # expect something like 'regs'

BASE = 0x4000_0000
CTRL0, CTRL1, STATUS, COUNTER = 0x00, 0x04, 0x08, 0x0C
mmio = MMIO(BASE, 0x1_0000)

print(hex(mmio.read(STATUS)))                  # 0x1a2b0000 after reset

mmio.write(CTRL0, 0x0000_00A1)                 # LEDs 1010, counter enabled
a = mmio.read(COUNTER)
time.sleep(0.5)
b = mmio.read(COUNTER)
print(f'counted {(b - a) & 0xFFFF_FFFF} clocks in about half a second')

mmio.write(CTRL1, 1_000_000)                   # compare value
mmio.write(CTRL0, 0x0000_0003)                 # clear, then run
time.sleep(0.01)
print('reached' if mmio.read(STATUS) & 0x2 else 'not yet')

オーバーレイを読み込むと、ビットストリームからPLが書き込まれます。最初の読み出しで0x1a2b0000、最初の書き込み後にLED 1と3が点灯、0.5秒の間隔で50,000,000に近いカウント(Pythonのsleepは正確ではないので数パーセント前後します)、そしてreachedが出るはずです。最初の読み出しが0x0を返す場合は、アドレスがブロックデザインと一致していません。ol.ip_dict['regs']['phys_addr']を確認してください。

VitisのCから

Vitisで、regs.xsaからps7_cortexa9_0上でstandaloneオペレーティングシステムを使うプラットフォームを作成し、そのプラットフォーム上にHello Worldテンプレートからアプリケーションを作成します。helloworld.cを次の内容に置き換えます。

#include <stdio.h>
#include "xil_io.h"
#include "xil_printf.h"
#include "sleep.h"

#define REGS_BASE   0x40000000u
#define REG_CTRL0   (REGS_BASE + 0x00u)
#define REG_CTRL1   (REGS_BASE + 0x04u)
#define REG_STATUS  (REGS_BASE + 0x08u)
#define REG_COUNTER (REGS_BASE + 0x0Cu)

int main(void)
{
    xil_printf("status 0x%08x\r\n", Xil_In32(REG_STATUS));

    Xil_Out32(REG_CTRL0, 0x000000A1u);          /* LEDs 1010, counter enabled */
    u32 a = Xil_In32(REG_COUNTER);
    usleep(100000);                              /* 100 ms */
    u32 b = Xil_In32(REG_COUNTER);
    xil_printf("counted %u clocks in 100 ms\r\n", b - a);

    Xil_Out32(REG_CTRL1, 1000000u);
    Xil_Out32(REG_CTRL0, 0x00000003u);          /* clear, then run */
    usleep(10000);
    xil_printf("status 0x%08x\r\n", Xil_In32(REG_STATUS));

    for (;;) {                                   /* walk the LEDs */
        for (u32 i = 0; i < 4; i++) {
            Xil_Out32(REG_CTRL0, (1u << (4 + i)) | 1u);
            usleep(250000);
        }
    }
    return 0;
}

ビルドし、ボードのUARTに115,200ボーで端末を接続して、Run on Hardwareを実行します。VitisがJTAG経由でビットストリームを書き込み、プログラムをロードします。端末にはステータスワード、100 msの間にカウントしたクロック数(10,000,000に近い値)、ビット1がセットされたステータスワードが順に表示され、4つのLEDがLD0からLD3へ順に点灯します。

試してみること

  • 割り込みを追加する。カウンタがctrl1に到達したときに1サイクルのパルスを出し、IRQ_F2Pポート経由でPSに接続して、Cで処理します。
  • 読み出し専用レジスタへの書き込みを無視する代わりにSLVERR(2'b10)を返し、PYNQとVitisがそれをどう報告するかを確認する。
  • ブロックを16レジスタに拡張し、YAMLの記述から短いPythonスクリプトでレジスタマップの表を生成する。
  • 手書きのスレーブをVivadoのCreate and Package IPウィザードで作ったものに置き換え、生成されたVerilogを自分のものと比較する。
  • カウンタ値を出力するAXI4-Stream出力を追加してDMAエンジンに接続する。これがSoC FPGAトピックへの道です。

まとめ

  • AXI4-Liteは5つのvalid/readyチャネルであり、書き込みアドレスとデータを両方そろうまで保持するスレーブは、どんなマスターにも対応できます。
  • モジュールリファレンスを使えば素のVerilogをブロックデザインに入れられ、ポート名の規約と2つの属性でVivadoがバスを推論します。
  • レジスタがどこに現れるかはAddress Editorが決めます。ソフトウェアはそのアドレスをハードコードするか、ハードウェアハンドオフから読み取ります。
  • PYNQのMMIOとVitisのXil_Out32は同じものです。AXIアドレス空間へのポインタです。
  • 識別子レジスタは、ペリフェラルが持てる最も安価なデバッグ補助です。