前回の記事でひとまずPYNQ-Z2のプログラムが作成できるようになりましたので,ここではPS-PL間のデータ通信を行います.
ハードとソフト
- ハード
- PYNQ-Z2
- FT234X 超小型USBシリアル変換モジュール (https://akizukidenshi.com/catalog/g/gM-08461/)
- ソフト
- Vivado 2022.2
- Jupyter Notebook (PYNQ-Z2上)
- Tera Term Version4.106
PSで周期を指定し,Lチカ
PSからPLに対して周期を指定し,PLはその周期に従ってLチカするというものです.
こちらの資料に従い作業を進めたのですが,タイミングエラーが出てしまったため大筋だけ真似つつRTLモジュールは自分で作ったら動きました.
なぜうまくいったのかは分からないです.
タイミングエラーが出た方
タイミングエラーが出た方のプロジェクトについても一応載せておきます.
module pynq_blink(
input wire clk,
input wire resetn,
input wire [31:0] cdiv,
output reg [3:0] led
);
reg [31:0] count, n_count;
reg [3:0] n_led;
always @(cdiv, count, clk) begin
if (count >= cdiv) begin
n_count <= 1;
n_led <= ~n_led;
end else begin
n_count <= n_count + 1'b1;
end
end
always @(posedge clk) begin
if (!resetn) begin
count <= 0;
led <= 4'b1001;
end else begin
count <= n_count;
led <= n_led;
end
end
endmodule
タイミングエラーが出なかった方
タイミングエラーが出なかった方です.
なおここでは,PCとPYNQ-Z2をLANケーブルで直接つなぐ方法を採用しています.その場合,上記資料18ページに記載されている設定が必要です.
また,上記資料ではGenerate Block Design
が必要であると書かれていましたが,しなくても動きました.
Generate Bitstream
ののち,上記資料40~41ページに従い各ファイルをPYNQ-Z2にアップロードしました.
なお,私の使用したVivadoではhwhファイルはプロジェクト名.srcs\sources_1\bd\design_1\hw_handoff\design1.hwh
ではなくプロジェクト名.gen\sources_1\bd\top\hw_handoff\top.hwh
にありました(ファイル名やディレクトリ名の一部が異なるのはブロックデザイン名が異なるからで,本題ではありません).
ブロックデザイン
上記の「タイミングエラーが出た方」と全く同じです.
RTLモジュール
周期を32ビットで受け取ってLチカ.
周期が0のとき,リセットと同様の動作をします.
module blink(
input clk,
input resetn,
input [31:0] cycle, // 周期
output reg [3:0] led // LチカするLED
);
reg [31:0] cnt = 32'b1; // Lチカ用のカウンタ
always @(posedge clk) begin
// リセット動作
if (!resetn || cycle == 32'b0) begin
cnt <= 32'b1;
led <= 4'b1001;
// 通常動作
end else begin
// カウントが周期を超えれば,反転
if (cnt >= cycle) begin
led <= ~led;
cnt <= 32'b1;
// それ以外ならカウントを進める
end else begin
cnt <= cnt + 32'b1;
end
end
end
endmodule
制約ファイル
##LEDs
set_property -dict { PACKAGE_PIN R14 IOSTANDARD LVCMOS33 } [get_ports { led[0] }]; #IO_L6N_T0_VREF_34 Sch=led[0]
set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports { led[1] }]; #IO_L6P_T0_34 Sch=led[1]
set_property -dict { PACKAGE_PIN N16 IOSTANDARD LVCMOS33 } [get_ports { led[2] }]; #IO_L21N_T3_DQS_AD14N_35 Sch=led[2]
set_property -dict { PACKAGE_PIN M14 IOSTANDARD LVCMOS33 } [get_ports { led[3] }]; #IO_L23P_T3_35 Sch=led[3]
Pythonプログラム
Overlay
クラスを用い,引数に与えた名前のビットファイルをFPGAに書き込みます.
gpio.write
がレジスタへの書き込みです.手前のgpio = pl.axi_gpio_0
でブロックデザイン内のaxi_gpio_0
モジュールをPython内で扱うことができ,write
メソッドを用いることでレジスタに値を書き込めます.一つ目の引数がアドレスで,ふたつめが書き込む値です.バスが一束しかないので一つ目の引数は0以外取りようがないと思います.
このプログラムでは,まず100Mを送って1秒周期にLチカ,2秒後に25Mを送って0.25秒周期,さらに2秒後に1.25秒周期のLチカをさせたあと,リセット状態で終了です.
from pynq import Overlay
from time import sleep
# ビットファイル読み込み
pl = Overlay("blink.bit")
# モジュール読み込み
gpio = pl.axi_gpio_0
# いろいろな周期を書き込む
gpio.write(0, 100 * 10**6); sleep(2)
gpio.write(0, 25 * 10**6); sleep(2)
gpio.write(0, 125 * 10**6); sleep(2)
gpio.write(0, 0)
実行結果
映像撮るの忘れました.
もし撮ったら載せますが,プログラム通りの動作なので大層な映像にはならないと思います…
PS->PL->UART
Jupyter Notebookを使ってPSにデータを送ると,即座にPLにも共有し,それをシリアル通信で送信するプログラムです.PYNQ-Z2から出た信号をシリアル変換モジュールでPCへ届け,それをTera Termで読みます.
シリアル通信を使う意味は特にないです,メインはPSからPLにデータを送るところです.
ブロックデザイン
recieve_char_0
はaxi_gpio_0
のgpio_io_o
を見張り,変化があればFIFOに書き込む.
uart_send_0
はFIFOからデータを受け取り,シリアル送信する.
RTLモジュール
recieve_char
PSとつながっているレジスタを常に見張り,変化があればAXISを用いてFIFOに送り込むモジュールです.
module recieve_char(
input clk,
input resetn,
input [7:0] data, // PSから受け取るデータ
// m_axis
output [7:0] gpio_tdata, // FIFOに送るデータ
input gpio_tready, // 受信準備完了信号
output reg gpio_tvalid // 送信準備完了信号
);
// PSからFIFOへ,データは直接送る
assign gpio_tdata = data;
reg [7:0] pre_data = 8'b0; // 前回観測時のデータ
always @(posedge clk) begin
// 前回観測時のデータを更新
pre_data <= data;
// リセット動作
if (!resetn) begin
gpio_tvalid <= 1'b0;
// 通常動作
end else begin
// PSから受け取るデータに変化があれば,FIFOにデータ送信
if (data != pre_data) begin
gpio_tvalid <= 1'b1;
end
// データ送信モードで,かつFIFOがそれを受信すれば送信終了
if (gpio_tvalid && gpio_tready) begin
gpio_tvalid <= 1'b0;
end
end
end
endmodule
uart_send
FIFOにデータが来ると,それをAXISで受け取ってシリアル通信で送信するモジュールです.
module uart_send(
input clk,
input resetn,
// serial
input rx,
output reg tx,
// s_axis
input [7:0] char_tdata, // FIFOから受け取るデータ
output reg char_tready, // 受信準備完了信号
input char_tvalid // 送信準備完了信号
);
// シリアル通信のための,現在の状態定数
parameter [2:0] IDLE = 3'b00; // 待機
parameter [2:0] RECIEVE = 3'b001; // FIFOから受信
parameter [2:0] START = 3'b010; // スタートビット送信
parameter [2:0] SEND = 3'b011; // データ送信
parameter [2:0] CLOSE = 3'b100; // エンドビット送信
parameter integer DIV = 32'd100; // 1MHzのシリアル通信をするための分周量
reg [31:0] count = 32'b1; // 分周のためのカウンタ
reg [7:0] data = 8'b0; // シリアル送信するデータ
reg [2:0] place = 3'd7; // シリアル送信するデータの場所(8ビットのうち,どこなのか)
// 状態の初期値として「待機」を入れる
reg [2:0] state = IDLE;
always @(posedge clk) begin
// リセット動作
if (!resetn) begin
tx <= 1'b1;
char_tready <= 1'b0;
count <= 32'b1;
data <= 8'b0;
place <= 3'd7;
state <= IDLE;
// 通常動作
end else begin
case (state)
// 待機
IDLE: begin
// FIFOが送信モードかつ自身が受信モードなら
if (char_tvalid && char_tready) begin
char_tready <= 1'b0;
data <= char_tdata;
state <= START;
// それ以外
end else begin
char_tready <= 1'b1;
end
end
// スタートビット送信
START: begin
if (count == DIV) begin
count <= 32'b1;
state <= SEND;
end else begin
count <= count + 32'b1;
end
tx <= 1'b0;
end
// データを1ビットずつ送信
SEND: begin
if (count == DIV) begin
count <= 32'b1;
place <= place - 3'b1;
if (place == 3'b0) begin
// (placeは0~7の値しかとらないので,ここでリセットする必要はない)
state <= CLOSE;
end
end else begin
count <= count + 32'b1;
tx <= data[place];
end
end
// エンドビット送信
CLOSE: begin
if (count == DIV) begin
count <= 32'b1;
state <= IDLE;
end else begin
count <= count + 32'b1;
end
tx <= 1'b1;
end
endcase
end
end
endmodule
制約ファイル
rxとtxだけです.
set_property -dict { PACKAGE_PIN V17 IOSTANDARD LVCMOS33 } [get_ports { rx }]; #IO_L21P_T3_DQS_34 Sch=ar[8]
set_property -dict { PACKAGE_PIN V18 IOSTANDARD LVCMOS33 } [get_ports { tx }]; #IO_L21N_T3_DQS_34 Sch=ar[9]
Pythonプログラム
とりあえずdata.write(0, 42)
としてますが,この部分を別セルにして数字を色々と変えながら動かしました.
from pynq import Overlay
# ビットファイル読み込み
pl = Overlay("uart_send.bit")
# モジュール読み込み
data = pl.axi_gpio_0
# 文字コード書き込み(この部分だけ別セルに置いておいて,文字コード部分を色々変えながら実行した)
data.write(0, 42)
実行結果
PYNQ-Z2のtxとrxをシリアル通信モジュールで接続し,Tera Termを用いてPYNQ-Z2からのシリアル通信を読み取りました.
映像とかはないですが,きちんと動きました.
UART->PL->PS
シリアル通信をPLが受けると,即座にデータをPSにも共有し,それをJupyter Notebook上で見られるというものです.PYNQ-Z2へのシリアル通信はシリアル変換モジュールを使い,PCのTera Termから行いました.
こちらもシリアル通信はメインではないです,本題はPLからPSへデータを送るところです.
ブロックデザイン
uart_read_0
はシリアル通信を受け取ると,そのデータをFIFOへ送る.
receive_char_0
はFIFOにデータが来ると,そのデータを受け取りaxi_gpio_0
に届ける.
RTLモジュール
uart_read
シリアル通信が来るまで待機し,1バイト受け取るとそれをFIFOに送るモジュール.
シリアル通信の受信は,動作の安定性のためにflagという変数を追加しているので,送信と比べるとやや複雑です(後述).
このプログラムはコメントだけではわかりづらいと思うので,手書きですが下に図を用意しました.合わせてご覧ください.
module uart_read(
input clk,
input resetn,
// serial
input rx,
output tx,
// m_axis
output reg [7:0] uart_tdata, // シリアル通信で受け取ったデータ
input uart_tready, // 受信準備完了信号
output reg uart_tvalid // 送信準備完了信号
);
// 送信はしないのでHIGH固定.
assign tx = 1'b1;
// 受信のための状態定義
parameter [1:0] IDLE = 2'b00; // 待機
parameter [1:0] START = 2'b01; // スタートビット
parameter [1:0] READ = 2'b10; // データ受け取り
parameter [1:0] CLOSE = 2'b11; // エンドビットとFIFOへの送信
parameter integer CYCLE = 32'd100; // 1MHz
parameter integer DIV = CYCLE / 2; // 1MHzのための分周
reg [31:0] count = 32'b1; // 分周のためのカウンタ
reg [7:0] temp = 8'b0; // 受信データを一時的に置くところ
reg [7:0] mask = 8'b1; // 今受け取っているデータが何ビット目か
reg [1:0] state = IDLE; // 状態は「待機」で初期化
reg mode = 1'b0; // 受信モードかどうか
reg flag = 1'b1; // 通信安定のためのフラグ(後述)
reg pre_rx = 1'b1; // 前回のrxの値(立下り検出に用いる)
always @(posedge clk) begin
// 前回のrxの値を更新
pre_rx <= rx;
// リセット動作
if (!resetn) begin
count <= 32'b1;
temp <= 8'b0;
mask <= 8'b1;
flag <= 1'b1;
mode <= 1'b0;
state <= IDLE;
uart_tdata <= 8'b0;
uart_tvalid <= 1'b0;
// 通常動作
end else begin
// シリアル通信の受信モードなら
if (mode) begin
// 分周(baudrateの二倍の周期)
if (count == DIV) begin
count <= 32'b1;
case (state)
// スタートビットを受け取るところ
// rxの立ち下がりを検出してから
// この時点で,スタートビット観測から半周期経っている
START: begin
flag <= !flag;
// flagの初期値は1なので,flagが0の状態でstateはREADに移る
if (flag) begin
state <= READ;
end
end
// データを受け取るところ
READ: begin
flag <= !flag;
// データが切り替わってから半周期のタイミングで信号読み取り
// 信号の読み取りは,maskをずらしながら行う
if (flag) begin
temp <= rx ? temp | mask : temp;
mask <= {mask[6:0], 1'b0};
if (mask == 8'h80) begin
state <= CLOSE;
end
end else begin
;
end
end
// エンドビットを受け取り,FIFOにデータを送信
CLOSE: begin
// 送信準備と受信準備が共に完了していれば
if (uart_tvalid && uart_tvalid) begin
// 各変数を初期状態に戻し,読み込みモード終了
flag <= 1'b1;
temp <= 8'b0;
mask <= 8'b1;
state <= IDLE;
mode <= 1'b0;
uart_tvalid <= 1'b0;
// 送信準備も受信準備も完了していなければ
end else begin
// 送信準備を整え,送信準備完了信号を1に
uart_tvalid <= 1'b1;
uart_tdata <= temp;
end
end
endcase
// 分周のためのカウント
end else begin
count <= count + 32'b1;
end
// シリアル通信の受信モードでないなら
end else begin
// 立ち下がりを検出したら
if ({pre_rx, rx} == 2'b10) begin
// 各変数を初期化し,シリアル受信モードをONにする
state <= START;
mode <= 1'b1;
flag <= 1'b1;
count <= 32'b1;
temp <= 8'b0;
mask <= 8'b1;
end
end
end
end
endmodule
シリアル通信を受信する図です.
横の一マスがシリアル通信の周期,sがスタートビット,eがエンドビットです.0xf0
という信号が送られてきています.
その時の,いくつかの変数の変遷を描いています.
立ち下がり検出時にmodeが1に,stateがSTARTになり,countがカウントされ始めます.それから半周期後にcount=DIVとなるので,flagが0になり,stateはREADになります.そのさらに半周期後,つまり立ち下がりから一周期後に再びcount=DIVとなりますが,flagが0なので何もしません.その後再びcount=DIVとなる,つまり立ち下がりから1.5周期後にやっとcount=DIVかつflag=1となり,**受信信号が変化してから半周期後の安定した信号を読み取れます.**その後もナントカ.0周期後はflagが0だから〜,ナントカ.5周期後はflagが1だから〜を繰り返します.
こんな複雑なことせず,変化直後の信号を読んでもなぜかうまくいくんですけどね.なんとなく怖いので一応やっています.
recieve_char
FIFOにデータが来ると,それをPSとの共有レジスタに書き込むモジュール.
module recieve_char(
input clk,
input resetn,
output reg [7:0] data, // 共有レジスタへデータを送るところ
// m_axis
input [7:0] gpio_tdata, // FIFOから受け取ったデータ
output reg gpio_tready, // 受信準備完了信号
input gpio_tvalid // 送信準備完了信号
);
always @(posedge clk) begin
// リセット動作
if (!resetn) begin
gpio_tready <= 1'b0;
gpio_tready <= 1'b0;
data <= 8'b0;
// 通常動作
end else begin
// FIFOの送信準備が完了したら
if (gpio_tvalid) begin
// 受信を行い,それが完了したことを知らせる
data <= gpio_tdata;
gpio_tready <= 1'b1;
// FIFOからの送信データがないときは
end else begin
// 受信準備が完了していないことにする
// (別にこうする必要はないけど,一応)
// (AXISの仕様上,どうせ送信側は受信側の事情なんて考えずにデータ送ってくるし…)
gpio_tready <= 1'b0;
end
end
end
endmodule
制約ファイル
こちらもrxとtxだけです.
set_property -dict { PACKAGE_PIN V17 IOSTANDARD LVCMOS33 } [get_ports { rx }]; #IO_L21P_T3_DQS_34 Sch=ar[8]
set_property -dict { PACKAGE_PIN V18 IOSTANDARD LVCMOS33 } [get_ports { tx }]; #IO_L21N_T3_DQS_34 Sch=ar[9]
Pythonプログラム
gpio.read
メソッドでPLとの共有レジスタの値を読むことができます.引数はレジスタのアドレスです.
レジスタの値が変化したらそれを表示しています.
from pynq import Overlay
# bitファイルを書き込み,モジュールを取得
pl = Overlay("uart_read.bit")
gpio = pl.axi_gpio_0
# 8ビットの情報をascii文字列にする
pre_data = chr(gpio.read(0))
try:
while True:
# 8ビットの情報をascii文字列にする
data = chr(gpio.read(0))
# もしデータが更新されていたら
if pre_data != data:
if data == "\n":
break
else:
print(data)
# 一ループ前の情報を保存
pre_data = data
except KeyboardInterrupt:
print("end")
# ^C以外のエラーが起きたときのため
# except:
# print("error")
実行結果
こちらはTera Termとシリアル変換モジュールを用いてPCからPYNQ-Z2へシリアル通信で文字を送り,それをPythonで置けとりました.
映像はないですが,Pythonの出力だけ載せておきます.送り込んだ文字をこんな感じで読み取って,そのデータをPSへ運んでくれました.
k
i
l
f
o
r
y
o
u
d
e
s
u
end
まとめ,というか感想
私はArty S7-50でもPS-PL間通信の経験がありますが,その時と比べると非常に簡単にできるという印象を受けました.
PYNQ-Z2でのPS-PL間通信はArty S7-50と比べてだいぶラッピングされているようです.おそらく根底の仕組みは似通っていると思いますので,PYNQ-Z2しか経験がないと,何をしているのかのイメージが全くわかずこんなにあっさりと達成はできていなかったのかなと思います(Arty S7-50の方も,完全に理解しているわけではないのですが).
これを使って何か面白いことしたい〜! という気持ちはあるのですが,肝心の「何か」がまだ決まっていないので,とりあえずそれが浮かぶまでは疑問の解消に努めたいと思います.
疑問
- なんでflagを使わなくても(変化直後のはずの信号を読んでも)シリアル受信がうまくいくの?
- なぜ似たようなことをやったのにタイミングエラーが出たり出なかったりしたの?
- PS-PL間通信のタイムラグはどのくらいなのか,どの程度の時間でレジスタ情報が共有されるのか