電光掲示板を作ろう(PYNQ-Z2シリーズ)

# ハードとソフト
- ハード
    - PYNQ-Z2
    - [安定化電源](https://www.amazon.co.jp/gp/product/B08GYJPC6T/ref=ppx_yo_dt_b_asin_title_o05_s01?ie=UTF8&th=1)
    - [マトリクスLED](https://www.amazon.co.jp/gp/product/B07PK5J21V/ref=ppx_yo_dt_b_asin_title_o07_s00?ie=UTF8&th=1)
- ソフト
    - Vivado 2022.2

# hub75eとは？
本題に入る前に，今回使用したマトリクスLEDについてざっくり説明します．
hub75はマトリクスLEDのインターフェースの規格で，hub75eはそれに「e」ピンが追加されたものです．hub75では行の指定が「a,b,c,d」ピンの4bitしかないので32行までしか点灯させられません(注: 16行ではない)が，hub75eなら5bitあるので64行表示できます(注: 32行ではない)．
以降はhub75eの説明になりますが，行を指定するビットが一つ減るだけで，本質的にはhub75も同じ感じです．
hub75eでは行を指定して一度に二行ずつ光らせます．例えば`5'b00000`のときは0行目と32行目が，`5'b00001`のときは1行目と33行目が，，，`5'b11111`のときは31行目と63行目が光ります．
各列の色はどう指定するかというと，RGBそれぞれで上側の色と下側の色(`5'b00000`のときは0行目の色と32行目の色)を指定するためのピンがあり，それぞれを`high`または`low`にします．その後クロックを立ち上げると一列分書き込まれ，また次の列のデータを指定してクロックを立ち上げ，，，を繰り返して64列のデータを指定します．
なお，点灯はRGBそれぞれを光らせる・光らせないの8色しかなく，一度に点灯させられるのも2行だけです．素早く色や行を切り替えてPWM表示・ダイナミック点灯することで，マトリクス全体にいろいろな色が表示されているように見せます．

# 目標
1. PYNQ-Z2のSDカードに画像データをのせよう
1. 画像データをPSで読み取ろう
1. 読み取った画像データをPLへ送ろう
1. 画像データをマトリクスLEDへ送ろう

# PYNQ-Z2のSDカードに画像データをのせよう
使用したマトリクスLEDが64x64なので，Pythonを使って正方形の画像を64x64に整形しました．
表示画像はこちら．

https://twitter.com/anime_oshinoko/status/1651231909509226497

上記GIF画像を`kana.gif`という名前でダウンロードし，画像一枚ずつに分け，64x64にしました．

```py
import cv2

SIZE = 64

# GIF画像読み込み
cap = cv2.VideoCapture("kana.gif")

# 一枚ずつ分けて保存
i = 0
while (cap.isOpened()):
    ret, frame = cap.read()
    if ret:
        # 正方形にする(縦長画像なので高さを横幅に合わせた)
        frame = frame[0:490, :, :]
        # 64x64にする
        frame = cv2.resize(frame, (SIZE, SIZE))
        # 保存
        cv2.imwrite(f"kana_img/{i:04d}.png", frame)
        i += 1
```

こうして作成した画像をJupyter Notebook経由でSDカードに保存しました．

# 画像データをPSで読み取ろう
少し後の話になるのですが，PYNQ-Z2のDMAをPythonでやろうとすると，pynqライブラリのバージョンが古くうまくいきませんでした．アップグレードも上手くいかなかったので潔く(?)C++で挑むことにしました．
PYNQ-Z2にC++のOpenCVをインストールしました．「linux opencv c++」とかで調べたらインストール方法が出てくると思うので，ここでは割愛．
余談ですが，PYNQ-Z2についての情報を調べようとしてもほとんどヒットしません．しかしlinuxでの方法を調べると，PYNQ-Z2に応用できそうな情報がたくさんヒットします．
これで画像データが読み込めるようになりました．実際のプログラムは次の章でやります．

# 読み取った画像データをPLへ送ろう
次にpynq_apiライブラリをインストールしました．基本的には[Github](https://github.com/mesham/pynq_api)のreadme.mdに従えばインストールできます．一つ注意点として，readmeにはリンカ指定は`-lpynq_api`と書かれていますが，正しくは`-lpynq`です．
## PSから送信
PSからPLへはDMAを使ってデータを送ります．ちなみにDMAはその逆，PLからPSへの送信もできますが今回は使いません．
DMAでは一度に送れるデータ量に制限があります．バースト転送することでたくさんのデータを送ります．
ここでは一度に64ビットの情報を送ります．
また，一画素あたり4ビットRGBで表現することにしたので，一画像あたり64(マトリクスLEDの行数)x64(マトリクスLEDの列数)x4(一画素のビット数)x3(RGB)=49152bit=6144Byteあります．一色当たり2048Byteです．
バースト長は256になります．

```send_img_data.cpp
#include <iostream>
#include <opencv2/opencv.hpp>

#define SUCCESS          (0)
#define IMG_READ_FAIL   (-1)
#define IMG_SIZE        (64)
#define BURST_SIZE     (256)

// 一枚の画像情報をPSからPLへ送信する関数
int send_img_data(
    PYNQ_AXI_DMA &dma,
    PYNQ_SHARED_MEMORY &memory,
    unsigned long long *data,
    const char *img_path
) {
    // 画像の読み込み
    cv::Mat img;
    try {
        img = cv::imread(img_path, -1);
        if (img.empty()) throw IMG_READ_FAIL;
    }
    catch (int e) {
        return e;
    }

    // 画像データをバッファに詰めて送信する
    cv::Vec3b pixel;
    unsigned char place = 0;
    int shift_num = 0;
    // RGBの三回ループ
    for (int c = 0; c < 3; c++) {
        // 初期化(たぶんなくてもいい)
        for (int i = 0; i < BURST_SIZE; i++) {
            data[i] = 0L;
        }
        // 二重ループで一画素ずつループ
        for (int i = 0; i < IMG_SIZE; i++) {
            for (int j = 0; j < IMG_SIZE; j++) {
                pixel = img.at<cv::Vec3b>(i, j);

                // 一画素分の情報8bitの上位4bitを取得
                data[place] |= (((unsigned long long)(pixel[c]) >> 4)) << shift_num;
                shift_num += 4;

                // 64ビット埋まったら次の要素へ
                if (shift_num == 64) {
                    place++;
                    shift_num = 0;
                }
            }
        }

        // DMAで送信
        PYNQ_writeDMA(&dma, &memory, 0, sizeof(long long) * BURST_SIZE);
        PYNQ_waitForDMAComplete(&dma, AXI_DMA_WRITE);
    }

    return SUCCESS;
}
```

メイン関数はこちら．
ちなみにDMAのアドレスはブロックデザインの下記画像から調べられます．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/4e8aa021-b935-7f47-e108-9738ba3943bf.png)

```kana.cpp
#include <iostream>
#include <string>
#include <sstream>
#include <chrono>
#include <thread>
#include <opencv2/opencv.hpp>
extern "C" {
#include <pynq_api.h>
}

#include "send_img_data.h"

using namespace std::chrono;

int main(void) {
    std::cout << "start" << std::endl;
    char bit_path[] = "./bit/ps2pl2hub_nc.bit";
    const int ADDR = 0x40400000;
    int rtn;

    // ビットファイルの準備
    PYNQ_loadBitstream(bit_path);

    // DMAの準備
    PYNQ_SHARED_MEMORY memory;
    PYNQ_allocatedSharedMemory(&memory, sizeof(long long) * BURST_SIZE, 1);

    PYNQ_AXI_DMA dma;
    PYNQ_openDMA(&dma, ADDR);

    unsigned long long *data = (unsigned long long *)memory.pointer;

    // 4回ループ(GIF画像の繰り返し数)
    for (int i = 0; i < 4; i++) {
        // 34回ループ(GIF画像の画像数)
        for (int j = 0; j < 34; j++) {
            // 開始時刻取得
            auto start = system_clock::now();

            // ファイル名作成
            std::stringstream ss;
            ss << std::setw(4) << std::setfill('0') << j;
            std::string file_name = "./img_kana/" + ss.str() + ".png";

            // 画像データを送る
            rtn = send_img_data(
                dma, memory, data,
                file_name.c_str()
            );
            if (rtn != SUCCESS) {
                std::cout << i << " " << j << " error" << std::endl;
                break;
            }

            // 70ms経つまで待つ
            while (
                duration_cast<milliseconds>(system_clock::now() - start).count() % 1000 < 70
            );
        }
    }

    // 真っ黒画像を送る
    rtn = send_img_data(
        dma, memory, data,
        "./img/black.png"
    );
    if (rtn != SUCCESS) {
        std::cout << "black error" << std::endl;
    }
    else {
        std::cout << "black send" << std::endl;
    }

    // DMAを閉じる
    PYNQ_closeDMA(&dma);
    PYNQ_freeSharedMemory(&memory);

    std::cout << "end" << std::endl;

    return 0;
}
```

ちなみにコンパイルコマンドはこんな感じです．OpenCVとpynq_apiを使うためにかなり長くなっています．

```sh
g++ -o kana kana.cpp -I /usr/local/include/opencv4 -l opencv_core -l opencv_objdetect -l opencv_highgui -l opencv_imgproc -l opencv_videoio -l opencv_imgcodecs -lpynq -lcma -lpthread
```

## PLで受信
ここからはHDLになります．
その前に，全体像を張ります．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/dc34f505-42e7-6101-8d21-a974cc96d07e.png)

<details><summary>axi_dmaの設定</summary>

右クリック-> `Add IP`から`AXI Direct Memory Access`をブロックデザインに追加します．
追加直後は以下のような見た目です．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/99632a55-b61b-f2be-3a31-af2876119257.png)

設定画面を開いて，以下のように設定します．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/f79ea9f8-8aa4-4b87-55e1-040808ef5f83.png)

ここで，それぞれの言葉の意味は以下のような感じです．

- Enable Scatter Gather Engine: DMAの複雑な機能が使えるらしいです．よく知りません．「scatter gather」でググると情報が出てきます．
- Enabe Micro DMA: これをオンにすると下のAllow Unallgned Transfersにチェックを入れられなくなります．
- Width of Buffer length Register: バッファの位置を指定するレジスタの幅．バッファが32bitのとき，このレジスタは5bit以上必要．
- Address Width: 送受信するデータのアドレス幅っぽい．
- Enable Read Channel: そのまま，受信チャンネルを有効化するかどうか．ここでいう「受信」はPL目線なので，PS->PL通信を有効にするか．
    - Memory Map Data Width: このIPがPSと通信するときのデータ幅．
    - Stream Data Width: このIPがPLと通信するときのデータ幅．
    - Max Burst Size: そのまま．最大バースト長．
- Enable Write Channel: そのまま．PL->PS通信を有効にするか．

あとは`ZYNQ`をダブルクリックして設定画面を開いて以下の設定(`HP Slave AXI Interface`のところ)をして`Auto ～～`をやれば配線できます．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1925706/eedf93ac-efcc-61df-3d5b-7eed0ba512ab.png)


</details>

受信部は`axi_dma_0`で，受け取った画像を`img_memory_wrapper_0`で保存しています．その中身はこちら(Qiitaではコードの色が変わらなかったので拡張子を「.v」としていますが，実際は「.sv」です)．

```img_memory.v
module img_memory_0(
    input clk,
    input resetn,

    input [3:0] btn,    // デバッグ用なので関係ない
    input [1:0] sw,     // デバッグ用なので関係ない
    output [7:0] ar,    // デバッグ用なので関係ない

    // s axis
    input [63:0] tdata,
    input tlast,
    input tvalid,
    output reg tready,

    input [4:0] row,                // 表示する行
    output [64 * 4 - 1:0] red_f,    // 赤の上側
    output [64 * 4 - 1:0] red_s,    // 赤の下側
    output [64 * 4 - 1:0] green_f,
    output [64 * 4 - 1:0] green_s,
    output [64 * 4 - 1:0] blue_f,
    output [64 * 4 - 1:0] blue_s
    );

    // 受信状態
    typedef enum logic [2:0] {
        IDLE,
        BLUE,
        GREEN,
        RED
    } state_t;

    reg [2:0][255:0][63:0] full_data;  // 画像データ(RGB，バースト長，送られてくる1データの三次元配列)
    reg [7:0] itr = 8'hff;

    state_t state = IDLE;

    always @(posedge clk) begin
        // 初期化(リセット状態)
        if (!resetn) begin
            tready <= 1'b0;

            for (logic [1:0] i = 0; i < 3; i++) begin
                for (logic [8:0] j = 0; j < 256; j++) begin
                    full_data[i][j] <= 64'b0;
                end
            end
            itr <= 8'hff;

            state <= IDLE;
        // 通常動作
        end else begin
            case (state)
                // 待機(データが送信されてくるのを待つ)
                IDLE: begin
                    tready <= 1'b0;
                    if (tvalid) begin
                        state <= BLUE;
                        itr <= 8'hff;
                    end
                end

                // 青色を取得
                BLUE: begin
                    if (tvalid) begin
                        tready <= 1'b1;
                        full_data[0][itr] <= tdata;
                        if (tlast) begin
                            state <= GREEN;
                            itr <= 8'hff;
                        end else begin
                            itr <= itr + 8'b1;
                        end
                    end else begin
                        tready <= 1'b0;
                    end
                end

                // 緑色を取得
                GREEN: begin
                    if (tvalid) begin
                        tready <= 1'b1;
                        full_data[1][itr] <= tdata;
                        if (tlast) begin
                            state <= RED;
                            itr <= 8'hff;
                        end else begin
                            itr <= itr + 8'b1;
                        end
                    end else begin
                        tready <= 1'b0;
                    end
                end

                // 赤色を取得
                RED: begin
                    if (tvalid) begin
                        tready <= 1'b1;
                        full_data[2][itr] <= tdata;
                        if (tlast) begin
                            state <= IDLE;
                            itr <= 8'hff;
                        end else begin
                            itr <= itr + 8'b1;
                        end
                    end else begin
                        tready <= 1'b0;
                    end
                end
            endcase
        end
    end

    // 指定された行のデータを出力(4要素で一行分のデータになる)
    assign blue_f  = {full_data[0][{1'b0, row, 2'b11}], full_data[0][{1'b0, row, 2'b10}], full_data[0][{1'b0, row, 2'b01}], full_data[0][{1'b0, row, 2'b00}]};
    assign blue_s  = {full_data[0][{1'b1, row, 2'b11}], full_data[0][{1'b1, row, 2'b10}], full_data[0][{1'b1, row, 2'b01}], full_data[0][{1'b1, row, 2'b00}]};
    assign green_f = {full_data[1][{1'b0, row, 2'b11}], full_data[1][{1'b0, row, 2'b10}], full_data[1][{1'b0, row, 2'b01}], full_data[1][{1'b0, row, 2'b00}]};
    assign green_s = {full_data[1][{1'b1, row, 2'b11}], full_data[1][{1'b1, row, 2'b10}], full_data[1][{1'b1, row, 2'b01}], full_data[1][{1'b1, row, 2'b00}]};
    assign red_f   = {full_data[2][{1'b0, row, 2'b11}], full_data[2][{1'b0, row, 2'b10}], full_data[2][{1'b0, row, 2'b01}], full_data[2][{1'b0, row, 2'b00}]};
    assign red_s   = {full_data[2][{1'b1, row, 2'b11}], full_data[2][{1'b1, row, 2'b10}], full_data[2][{1'b1, row, 2'b01}], full_data[2][{1'b1, row, 2'b00}]};

    // デバッグ用なので関係ない
    assign ar = sw != 2'b11 ? full_data[sw][btn][7:0] : 8'b0;

endmodule
```

# 画像データをマトリクスLEDへ送ろう
これまでのところで，PYNQ-Z2のSDカードに保存されている画像データをPL上のレジスタに展開できたので，今度はそのデータをマトリクスLEDに送信します．保存されている画像データを絶えず送信し続けます．
例によって，実際の拡張子は「.sv」です．

```hub_driver.v
module hub_driver(
    input clk,
    input resetn,

    // 一行分のデータ(上側)
    input [64 * 4 - 1:0] blue_in_f,
    input [64 * 4 - 1:0] green_in_f,
    input [64 * 4 - 1:0] red_in_f,

    // 一行分のデータ(下側)
    input [64 * 4 - 1:0] blue_in_s,
    input [64 * 4 - 1:0] green_in_s,
    input [64 * 4 - 1:0] red_in_s,

    // hub75eへ送信するデータ
    output reg [4:0] row,   // hub75eへ送るとともに，img_memoryモジュールに行を指定している
    output reg [1:0] red,
    output reg [1:0] green,
    output reg [1:0] blue,
    output reg lcl, // led clk
    output reg lat,
    output reg oe
    );

    typedef enum logic [1:0] {
        IDLE,
        SEND_ROW,
        LAT_HIGH,
        LAT_LOW
    } state_t;

    // hub75eへ送信するデータのクロック周波数
    localparam integer FREQ = 8_000_000 /* Hz */;
    localparam integer HALF_CYCLE = 100_000_000 / FREQ / 2;

    state_t state = IDLE;

    reg [31:0] cnt = 32'b1;

    reg carry_flag = 1'b0;
    reg [5:0] col = 6'b1;   // 列
    reg [3:0] step = 4'b0;  // 明るさを指定するための変数

    // それぞれ一行分のデータ
    reg [64 * 4 - 1:0] red_color_f = 256'b0;
    reg [64 * 4 - 1:0] blue_color_f = 256'b0;
    reg [64 * 4 - 1:0] green_color_f = 256'b0;
    reg [64 * 4 - 1:0] red_color_s = 256'b0;
    reg [64 * 4 - 1:0] blue_color_s = 256'b0;
    reg [64 * 4 - 1:0] green_color_s = 256'b0;

    always_ff @(posedge clk) begin
        if (!resetn) begin
            row <= 5'd0;
            red <= 2'b0;
            green <= 2'b0;
            blue <= 2'b0;
            lcl <= 1'b0;
            lat <= 1'b1;
            oe <= 1'b1;

            state <= IDLE;

            cnt <= 32'b1;

            carry_flag <= 1'b0;
            col <= 6'b1;
            step <= 4'b0;

            red_color_f <= 256'b0;
            blue_color_f <= 256'b0;
            green_color_f <= 256'b0;
            red_color_s <= 256'b0;
            blue_color_s <= 256'b0;
            green_color_s <= 256'b0;
        end else begin
            case (state)
                // 初期化
                IDLE: begin
                    lat <= 1'b0;
                    oe <= 1'b0;
                    state <= SEND_ROW;
                    step <= step + 4'b1;
                    frame_cnt <= 32'b1;
                    red_color_f <= red_in_f;
                    green_color_f <= green_in_f;
                    blue_color_f <= blue_in_f;
                    red_color_s <= red_in_s;
                    green_color_s <= green_in_s;
                    blue_color_s <= blue_in_s;
                end

                SEND_ROW: begin
                    if (cnt == HALF_CYCLE) begin
                        lcl <= !lcl;
                        cnt <= 32'b1;
                        // 立下りのタイミングで送信データを更新する
                        if (lcl) begin
                            // 4bitのデータがstepを超えているかどうかで出力をhighにするかどうか決める．
                            // stepはインクリメントされていくので，明るい画素ほど点灯される時間が長い
                            red <= {red_color_s[3:0] > step, red_color_f[3:0] > step};
                            green <= {green_color_s[3:0] > step, green_color_f[3:0] > step};
                            blue <= {blue_color_s[3:0] > step, blue_color_f[3:0] > step};
                            // 次の画素データを最下位ビットに移動させる
                            red_color_f   <= {4'b0, red_color_f[64 * 4 - 1:4]};
                            green_color_f <= {4'b0, green_color_f[64 * 4 - 1:4]};
                            blue_color_f  <= {4'b0, blue_color_f[64 * 4 - 1:4]};
                            red_color_s   <= {4'b0, red_color_s[64 * 4 - 1:4]};
                            green_color_s <= {4'b0, green_color_s[64 * 4 - 1:4]};
                            blue_color_s  <= {4'b0, blue_color_s[64 * 4 - 1:4]};
                            {carry_flag, col} <= col + 7'b1;
                            // 64列送り終えたら状態更新
                            if (carry_flag) begin
                                state <= LAT_HIGH;
                                lat <= 1'b1;
                                oe <= 1'b1;
                            end
                        end
                    end else begin
                        cnt <= cnt + 32'b1;
                    end
                end

                // 以下，点灯処理と次の行の表示準備
                LAT_HIGH: begin
                    if (cnt == HALF_CYCLE) begin
                        cnt <= 32'b1;
                        lat <= 1'b0;
                        oe <= 1'b0;
                        state <= LAT_LOW;
                        red_color_f <= red_in_f;
                        green_color_f <= green_in_f;
                        blue_color_f <= blue_in_f;
                        red_color_s <= red_in_s;
                        green_color_s <= green_in_s;
                        blue_color_s <= blue_in_s;
                    end else begin
                        cnt <= cnt + 32'b1;
                    end
                end

                LAT_LOW: begin
                    if (cnt == HALF_CYCLE) begin
                        cnt <= 32'b1;
                        row <= row + 5'b1;
                        // 画像一枚分を送り終えたら，stepを増やしもう一度送信
                        if (row == 5'd31) begin
                            state <= IDLE;
                        end else begin
                            state <= SEND_ROW;
                        end
                    end else begin
                        cnt <= cnt + 32'b1;
                    end
                end
            endcase
        end
    end

endmodule
```

# その他
System Verilogはブロックデザインに追加できないので，Verilog HDLでラッパーを作っています．

```img_memory_wrapper.v
module img_memory_wrapper(
    input clk,
    input resetn,
    
    input [3:0] btn,    // デバッグ用なので関係ない
    input [1:0] sw,     // デバッグ用なので関係ない
    output [7:0] ar,    // デバッグ用なので関係ない

    // s axis
    input [63:0] tdata,
    input tlast,
    input tvalid,
    output tready,

    input [4:0] row,
    output [64 * 4 - 1:0] red_f,
    output [64 * 4 - 1:0] red_s,
    output [64 * 4 - 1:0] green_f,
    output [64 * 4 - 1:0] green_s,
    output [64 * 4 - 1:0] blue_f,
    output [64 * 4 - 1:0] blue_s
    );

    img_memory_0 img_memory (
        .clk(clk), .resetn(resetn),
        .btn(btn), .sw(sw), .ar(ar),
        .tdata(tdata), .tlast(tlast), .tvalid(tvalid), .tready(tready),
        .row(row + 2),  // 次の行のデータが欲しいので，ここで調整
        .red_f(red_f), .red_s(red_s),
        .green_f(green_f), .green_s(green_s),
        .blue_f(blue_f), .blue_s(blue_s)
    );

endmodule
```

```hub_driver_wrapper.v
module hub_driver_wrapper(
    input clk,
    input resetn,
    
    input [64 * 4 - 1:0] red_in_f,
    input [64 * 4 - 1:0] red_in_s,
    input [64 * 4 - 1:0] green_in_f,
    input [64 * 4 - 1:0] green_in_s,
    input [64 * 4 - 1:0] blue_in_f,
    input [64 * 4 - 1:0] blue_in_s,

    output [4:0] row,
    output [1:0] red,
    output [1:0] green,
    output [1:0] blue,
    output lcl, // led clk
    output lat,
    output oe,
    output gnd
    );

    assign gnd = 1'b0;

    hub_driver hub_driver_0 (
        .clk(clk), .resetn(resetn),
        .blue_in_f(blue_in_f), .green_in_f(green_in_f), .red_in_f(red_in_f),
        .blue_in_s(blue_in_s), .green_in_s(green_in_s), .red_in_s(red_in_s),
        .row(row),
        .red(red), .green(green), .blue(blue),
        .lcl(lcl), .lat(lat), .oe(oe)
    );

endmodule
```

あとXDC.

```top.xdc
##PmodA

set_property -dict { PACKAGE_PIN Y18   IOSTANDARD LVCMOS33 } [get_ports { red[0] }]; #IO_L17P_T2_34 Sch=ja_p[1]
set_property -dict { PACKAGE_PIN Y19   IOSTANDARD LVCMOS33 } [get_ports { blue[0] }]; #IO_L17N_T2_34 Sch=ja_n[1]
set_property -dict { PACKAGE_PIN Y16   IOSTANDARD LVCMOS33 } [get_ports { red[1] }]; #IO_L7P_T1_34 Sch=ja_p[2]
set_property -dict { PACKAGE_PIN Y17   IOSTANDARD LVCMOS33 } [get_ports { blue[1] }]; #IO_L7N_T1_34 Sch=ja_n[2]
set_property -dict { PACKAGE_PIN U18   IOSTANDARD LVCMOS33 } [get_ports { green[0] }]; #IO_L12P_T1_MRCC_34 Sch=ja_p[3]
set_property -dict { PACKAGE_PIN U19   IOSTANDARD LVCMOS33 } [get_ports { gnd }]; #IO_L12N_T1_MRCC_34 Sch=ja_n[3]
set_property -dict { PACKAGE_PIN W18   IOSTANDARD LVCMOS33 } [get_ports { green[1] }]; #IO_L22P_T3_34 Sch=ja_p[4]
set_property -dict { PACKAGE_PIN W19   IOSTANDARD LVCMOS33 } [get_ports { row[4] }]; #IO_L22N_T3_34 Sch=ja_n[4]

##PmodB

set_property -dict { PACKAGE_PIN W14   IOSTANDARD LVCMOS33 } [get_ports { row[0] }]; #IO_L8P_T1_34 Sch=jb_p[1]
set_property -dict { PACKAGE_PIN Y14   IOSTANDARD LVCMOS33 } [get_ports { row[2] }]; #IO_L8N_T1_34 Sch=jb_n[1]
set_property -dict { PACKAGE_PIN T11   IOSTANDARD LVCMOS33 } [get_ports { lcl }]; #IO_L1P_T0_34 Sch=jb_p[2]
set_property -dict { PACKAGE_PIN T10   IOSTANDARD LVCMOS33 } [get_ports { oe }]; #IO_L1N_T0_34 Sch=jb_n[2]
set_property -dict { PACKAGE_PIN V16   IOSTANDARD LVCMOS33 } [get_ports { row[1] }]; #IO_L18P_T2_34 Sch=jb_p[3]
set_property -dict { PACKAGE_PIN W16   IOSTANDARD LVCMOS33 } [get_ports { row[3] }]; #IO_L18N_T2_34 Sch=jb_n[3]
set_property -dict { PACKAGE_PIN V12   IOSTANDARD LVCMOS33 } [get_ports { lat }]; #IO_L4P_T0_34 Sch=jb_p[4]
set_property -dict { PACKAGE_PIN W13   IOSTANDARD LVCMOS33 } [get_ports { gnd }]; #IO_L4N_T0_34 Sch=jb_n[4]
```

# 完成品
実はランダムでPS->PLのデータ送受信に失敗するのでたまに表示が乱れます．
おそらく回路のどこかに非効率な部分があると思うのですが，心折れたのでこの辺で完成としました．

https://x.com/tt41357393/status/1700806396147884426?s=20