PYNQ-Z2(またはZybo Z7)で作る初めてのカスタムAXIペリフェラル
4つのレジスタを持つAXI4-Liteスレーブを手書きし、Zynqのブロックデザインに組み込んでハードウェアをエクスポートし、PYNQ上のPythonとVitisのベアメタルCの両方から駆動します。
| ボード | 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に組み合わせ的に依存させてはならないことです。
ここで作るスレーブは、アドレスまたはデータをまだ保持していなければいつでも受け取り、それぞれをレジスタに覚えておき、両方がそろった時点で書き込みを実行します。これで、アドレスを先に送るマスター、データを先に送るマスター、両方を同時に送るマスターのすべてに対応できます。Zynqの汎用ポートは両方を同時に送りますが、テストベンチではもう一方の順序も試します。応答チャネルは書き込み後にbvalidを上げ、マスターがbreadyで受け取ったら下げます。
読み出しはもっと単純です。スレーブはデータを提示中でなければいつでもアドレスを受け取り、選択した値をrvalidをハイにしてrdataにレジスタ出力し、rreadyで受け取られたらrvalidを下げます。デコードするのはアドレスのビット3:2だけです。アドレスがこのスレーブの範囲内にあることはインターコネクトが既に確認しているので、上位ビットはここでは冗長です。
システム全体
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のブロックデザインを作る
- Create Projectで、RTL projectを選び、Boardsからお使いのボードを選択します。
- Add Sourcesで
axi_lite_regs.vを追加します。Add ConstraintsでXDCを追加します。 - Create Block Designを実行します。Add IPでZYNQ7 Processing Systemを追加します。Run Block Automationをクリックして既定値を受け入れると、ボードプリセットが適用されます。
- PSをダブルクリックし、PS-PL Configurationで
M AXI GP0 interfaceが有効になっていることを確認します。Clock ConfigurationでFCLK_CLK0が100 MHzであることを確認します。 - キャンバスを右クリックしてAdd Moduleを選び、
axi_lite_regsを選択します。ポート名がAXIの命名パターンに従っているので、Vivadoは単一のs_axiバスポートとして表示します。 - Run Connection Automationを実行し、
s_axiにチェックを入れて受け入れます。インターコネクトとリセットブロックが現れます。 ledピンを右クリックしてMake Externalを選びます。XDCに合わせてポート名をledに変更します。- Address Editorで、スレーブのオフセットを
0x4000_0000、範囲を64 Kに設定します。 - Validate Designを実行し、Sourcesでブロックデザインを右クリックしてCreate HDL Wrapperを選び、Vivadoに管理させます。
- 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アドレス空間へのポインタです。 - 識別子レジスタは、ペリフェラルが持てる最も安価なデバッグ補助です。