PYNQでPS-PL間のデータ共有(PYNQ-Z2シリーズ)

[前回の記事](https://qiita.com/tt_and_tk/items/0661eb2df0966751c521)でひとまず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チカするというものです．

[こちらの資料](https://aitech.ac.jp/~dslab/nf/pub/20210903_SWEST_PYNQ_slides.pdf)に従い作業を進めたのですが，タイミングエラーが出てしまったため大筋だけ真似つつRTLモジュールは自分で作ったら動きました．

なぜうまくいったのかは分からないです．

# タイミングエラーが出た方
タイミングエラーが出た方のプロジェクトについても一応載せておきます．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/9532af68-2f80-d880-9b64-46107751324b.png)

```verilog
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`にありました(ファイル名やディレクトリ名の一部が異なるのはブロックデザイン名が異なるからで，本題ではありません)．

## ブロックデザイン
上記の「タイミングエラーが出た方」と全く同じです．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/7ff53dd2-c4c6-8d46-4623-bbb2f1d4a1eb.png)

## RTLモジュール
周期を32ビットで受け取ってLチカ．
周期が0のとき，リセットと同様の動作をします．

```verilog
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チカをさせたあと，リセット状態で終了です．

```py
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からデータを受け取り，シリアル送信する．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/2f62597b-5a7a-48ad-c34d-3ee9f47a1f62.png)

## RTLモジュール
### recieve_char
PSとつながっているレジスタを常に見張り，変化があればAXISを用いてFIFOに送り込むモジュールです．

```verilog
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で受け取ってシリアル通信で送信するモジュールです．

```verilog
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だけです．

```xdc
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)`としてますが，この部分を別セルにして数字を色々と変えながら動かしました．

```py
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`に届ける．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/47f8bd51-fe46-418c-1d20-a82041fa2fca.png)

## RTLモジュール
### uart_read
シリアル通信が来るまで待機し，1バイト受け取るとそれをFIFOに送るモジュール．
シリアル通信の受信は，動作の安定性のためにflagという変数を追加しているので，送信と比べるとやや複雑です(後述)．
このプログラムはコメントだけではわかりづらいと思うので，手書きですが下に図を用意しました．合わせてご覧ください．


```verilog
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だから〜を繰り返します．

![IMG_0432.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/f2e94603-d0fc-7194-4683-e48b5c16af13.jpeg)

~~こんな複雑なことせず，変化直後の信号を読んでもなぜかうまくいくんですけどね．なんとなく怖いので一応やっています．~~


### recieve_char
FIFOにデータが来ると，それをPSとの共有レジスタに書き込むモジュール．

```verilog
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だけです．

```xdc
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との共有レジスタの値を読むことができます．引数はレジスタのアドレスです．
レジスタの値が変化したらそれを表示しています．

```py
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間通信のタイムラグはどのくらいなのか，どの程度の時間でレジスタ情報が共有されるのか