[組み込み] Zynqでカメラ制御

# はじめに
今回は、FPGAとソフトの両方を開発しようという内容になります。
XilinxにはZynqというCPUとFPGAが1つのパッケージ化されたSoCがあります。
今回はそれを使用します。

作るモノは前からカメラ関連をやりたいと思っていたので、それを題材とします。

さすがに1からシステムを作るのは骨が折れるので、ベースを使用してそこから簡単な機能を追加という形にしていきます。

ベースはDigilentのpcam-5cというプロジェクトを使用します。

https://digilent.com/reference/programmable-logic/zybo-z7/demos/pcam-5c

# 動作環境
PC：M4 Macbook Pro(Sequoia 15.1)
SW IDE：Vitis 2023.1
FPGA IDE：Vivado 2023.1
ディスプレイ：HDMI接続できるやつ
評価ボード：Zybo Z-10(今買うと5万超えるんですね…めっちゃ値上がりしてる)
カメラモジュール：Pcam 5C(OV5640)
カメラモジュール仕様↓

https://cdn.sparkfun.com/datasheets/Sensors/LightImaging/OV5640_datasheet.pdf

:::note warn
実施する場合の注意点
・IDEはWindows版かLinux版しかないためどちらかで実施してください。
　また、そこそこのPCスペック(特にメモリ)を要求してきますのでご注意を。
　(今回に関して言うと、16GBあれば動くと思います。)
:::

私みたいにMac(Appleシリコン)でどうしてもやりたいんだ！という方は以下を実施すればできます。
※**ただし全然おすすめはしないです**

https://github.com/ichi4096/vivado-on-silicon-mac

# 既存システムの概要

ざっくりとしたシステム構成を書きます。

### システム構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/60275437-ea8b-4cde-a39a-38d859350127.png)

処理の流れとしては以下ですね。
・OV5640からMIPIでカメラデータを受け取る
・カメラデータをPL部で画像処理する
・DMAを使用しカメラデータをDDRへ保存する
・出力タイミングにあわせ、DDRからデータを抜き出し、TMDSでディスプレイに送信する

### クロック&リセット系統
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/ed88385b-aaf2-4f65-91e0-c88d9a910fe8.png)

### バス構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/270dca87-e0ab-444a-a43e-4251c41315f0.png)

### モジュール詳細(PL)
#### MIPI_D_PHY_RX/MIPI_CSI_2_RX
カメラからのMIPI信号を受信するモジュール。

D_PHYの仕様
・AXI4-Liteよりレジスタアクセス可能
・リファレンスクロックとして200MHzを入力

CSI_2の仕様
・AXI4-Liteよりレジスタアクセス可能
・出力のデータタイプはRAW10(データの並びはBGBG/GRGR)
・MIPI信号をAXI4-Stream(150MHz)へ変換し出力
・AXI4-Streamのフォーマット

#### AXI_BayreToRGB
ベイヤー配列をRGBに変換する(デモザイク)モジュール。

:::note warn
名前がBayerToRGBなので、てっきり出力がRGBの順で出てくると
思ったが違ったっぽい。ソースコードを見るとRBGの順。トラップですね…

![RBG順.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/1aa939ad-7041-44c1-bd7c-352ff0f4a840.png)

ではなんでこの順番かと言うと、後段のVideo OutモジュールがRBGの並びなのでこの順番にしているのだと思います。

![VideoOut入力.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/b647074f-76b1-4cda-9724-6ac70821f665.png)

:::

モジュールの仕様
・入力データサイズ：40bit(RBG10bit)
・入力データサンプル数：4/clk
・出力データサイズ：32bit(RBG10bit + 2bitsのパディング)
・出力データサンプル数：1/clk
・最大画素数：2048pix
・ラインバッファを持つ(1ライン分)
・TUSERはフレームスタートの意味を付与する
・TLASTはライン終了の意味を付与する

入力データのイメージ

![データ転送順.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/603fd582-9b65-4dd0-8b62-cb485d7cbe50.png)

デモザイクの処理のイメージ

![デモザイク処理イメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/25286cec-dc24-4e60-a18b-44cc79e7dcfe.png)


#### GammaCorrection
ガンマ補正を行うモジュール。

モジュールの仕様
・入出力IF：AXI4-Stream
・入力データサイズ：32bit(RBG10bits + 2bitsのパディング)
・入力データサンプル数：1/clk
・出力データサイズ：24bit(RGB8bits)
・出力データサンプル数：1/clk
・AXI4-Liteよりレジスタアクセス可能
　レジスタでガンマ補正値を変更できる(補正値はROM持ち)
　設定可能ガンマ補正値：1.0、1.5、1.8、2.2

#### axi_vdma(VDMA)
PLからのデータをPS制御でメモリへ送る(逆も然り)モジュール。

モジュールの仕様
・S2MMのデータ幅：24bit
　バーストサイズ：8
　ラインバッファサイズ：1024
　メモリマップのデータ幅：64
・MM2Sのデータ幅：24bit
　バーストサイズ：8
　ラインバッファサイズ：1024
　メモリマップのデータ幅：64
・フレームバッファを持つ(3つ)
・アドレス幅は32bit
・AXI4-Liteよりレジスタアクセス可能

#### v_tc(Video Timing Controller)
ビデオタイミングの生成/検出を行うモジュール。

モジュールの仕様
・ビデオタイミングの検出モードは使用せず、生成モードのみ使用
・ビデオフォーマットは720p
・AXI4-Liteよりレジスタアクセス可能

#### v_axi4s_vid_out(Video Out)
AXI4-Streamデータをビデオデータ(RGB)に変換し、出力するモジュール。
上記のvtcとセットで使用する方がいい。

モジュールの仕様
・AXI4-Streamのデータフォーマットは「RBG」
・FIFOの深さは1024

#### rgb2dvi
ビデオデータをTMDSに変換し、出力するモジュール。

モジュールの仕様
・リセット信号(aRst_n)は前段のv_axi4s_vid_outモジュールのロック信号を入力とする
・PixelClkとSerialClkを入力する必要があり、以下の関係であること
　SerialClk = PixelClk × 5
　※SerialClkは内部で作成も可

### モジュール詳細(PS)

#### ファイル構成
![ファイル構成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/30a1ee60-4c80-4f87-bd5c-169eb01a1b4d.png)

#### フローチャート
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/943369b9-aa92-434b-84f0-efa4649b747a.png)

#### シーケンス図(画像フォーマット変更時)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/4405f2a3-2614-476d-95ed-ff2cd385b710.png)

# 追加機能
ここからが本題になります。
簡単な機能を1つ追加したいと思います。

既存のシステムがカラー画像なので、これをグレースケール化する機能を追加します。

#### 仕様
・入出力のデータフォーマットはAXI4-Streamとする。
　中のデータ構成は前後のモジュールと同じ。
・クロックリセット構成は前後のモジュールと同じ。
・AXI4-Liteよりレジスタアクセス可能。
・グレースケール化の方法は以下の通り。とりあえず楽な最大値/最小値方法で実装する。

<table>
<caption>グレースケール化
<thead>
<tr><th>方法<th>内容
<tbody>
<tr><tr><td>最大値/最小値<td>RGBの最大値・最小値を取得し設定する方法
<tr><tr><td>平均値<td>RGBを単純に平均し設定する方法
<tr><tr><td>NTSC重み付け<td>NTSCという規格に準拠し設定する方法
<tr><tr><td>チャネル選択<td>RGBのどれか1つのチャネルを取得し設定する方法
</table>

#### ブロック図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/badb029a-ce77-4661-8e9b-0f94da2701c3.png)

上記が機能追加後のブロック図になります。
BayerToRGBモジュールとGammmaCorrectionモジュールの間に入れました。

#### クロック&リセット系統
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/15f8a42b-7c93-46ef-ac4e-cfd4c25d9794.png)

#### バス接続
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/96ce4a76-8e24-4bf2-a03f-c269ec6d455f.png)

#### フローチャート
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/41d090ff-486b-4f14-b6b5-ae191a55728e.png)

#### シーケンス図(画像フォーマット変更時)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/d2d416c3-a3cd-45a3-9f41-157029ac93bd.png)

# 実装

### ソースコード

https://github.com/kiiisy/pcam_hw

https://github.com/kiiisy/pcam_sw

### PL部

```verilog
module grayscale_top #(
    parameter integer C_S_AXIS_DATA_WIDTH = 32
) (
    input  wire        CLK,
    input  wire        RESET,
    // S AXI-Stream interface
    output wire        S_AXIS_VIDEO_TREADY,
    input  wire [C_S_AXIS_DATA_WIDTH-1:0] S_AXIS_VIDEO_TDATA,
    input  wire        S_AXIS_VIDEO_TVALID,
    input  wire        S_AXIS_VIDEO_TUSER,
    input  wire        S_AXIS_VIDEO_TLAST,

    // M AXI-Stream interface
    input  wire        M_AXIS_VIDEO_TREADY,
    output wire [C_S_AXIS_DATA_WIDTH-1:0] M_AXIS_VIDEO_TDATA,
    output wire        M_AXIS_VIDEO_TVALID,
    output wire        M_AXIS_VIDEO_TUSER,
    output wire        M_AXIS_VIDEO_TLAST,

    // Grayscale
    input wire         GRAYSCALE_EN,
    input wire         GRAYSCALE_PTN
);

    wire [ 9: 0] w_red;
    wire [ 9: 0] w_green;
    wire [ 9: 0] w_blue;
    reg  [ 9: 0] r_max;
    reg          r_axis_video_tready;
    reg          r_axis_video_tvalid;
    reg          r_axis_video_tuser;
    reg          r_axis_video_tlast;

    assign w_green = S_AXIS_VIDEO_TDATA[ 9: 0];
    assign w_blue  = S_AXIS_VIDEO_TDATA[19:10];
    assign w_red   = S_AXIS_VIDEO_TDATA[29:20];

    always @(posedge CLK or posedge RESET) begin
        if (RESET) begin
            r_max <= 10'd0;
        end else begin
            if (S_AXIS_VIDEO_TVALID) begin
                r_max <= (w_red > w_green && w_red > w_blue) ? w_red : (w_green > w_red && w_green > w_blue) ? w_green : w_blue;
            end else begin
                r_max <= 10'd0;
            end
        end
    end

    always @(posedge CLK or posedge RESET) begin
        if (RESET) begin
            r_axis_video_tready <= 1'b0;
            r_axis_video_tvalid <= 1'b0;
            r_axis_video_tuser  <= 1'b0;
            r_axis_video_tlast  <= 1'b0;
        end else begin
            r_axis_video_tready <= M_AXIS_VIDEO_TREADY;
            r_axis_video_tvalid <= S_AXIS_VIDEO_TVALID;
            r_axis_video_tuser  <= S_AXIS_VIDEO_TUSER;
            r_axis_video_tlast  <= S_AXIS_VIDEO_TLAST;
        end
    end

    assign S_AXIS_VIDEO_TREADY = (GRAYSCALE_EN) ? r_axis_video_tready : M_AXIS_VIDEO_TREADY;
    assign M_AXIS_VIDEO_TDATA  = (GRAYSCALE_EN) ? {2'b00, r_max[9:0], r_max[9:0], r_max[9:0]} : S_AXIS_VIDEO_TDATA;
    assign M_AXIS_VIDEO_TVALID = (GRAYSCALE_EN) ? r_axis_video_tvalid : S_AXIS_VIDEO_TVALID;
    assign M_AXIS_VIDEO_TUSER  = (GRAYSCALE_EN) ? r_axis_video_tuser : S_AXIS_VIDEO_TUSER;
    assign M_AXIS_VIDEO_TLAST  = (GRAYSCALE_EN) ? r_axis_video_tlast : S_AXIS_VIDEO_TLAST;

endmodule
```

### 解説(PL部)
まず、レジスタは以下になります。

GRAYSCALE_EN：0：OFF、1：ON
GRAYSCALE_PTN：使用していない

PTNはグレースケール化のパターンを割り振りたいと思って一応持っておきました。
実際は使ってないです。

処理としてはマスターからのTVALID信号で最大値を取得し、他の信号も一段ずらしてスレーブに投げているだけになります。


### 波形
以下が、シミュレーションの波形になります。

![grayscaleの波形.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/123f45f5-179c-48a2-be5a-bae6297a99a5.png)

最大値が選ばれて出ていますね。

### PS部

```c++
void pipeline_mode_change(AXI_VDMA<ScuGicInterruptController> &vdma_driver,
                          OV5640 &cam, VideoOutput &vid, Resolution res,
                          OV5640_cfg::mode_t mode)
{
    // Bring up input pipeline back-to-front
    {
        vdma_driver.resetWrite();
        MIPI_CSI_2_RX_mWriteReg(XPAR_MIPI_CSI_2_RX_0_S_AXI_LITE_BASEADDR,
                                CR_OFFSET, (CR_RESET_MASK & ~CR_ENABLE_MASK));
        MIPI_D_PHY_RX_mWriteReg(XPAR_MIPI_D_PHY_RX_0_S_AXI_LITE_BASEADDR,
                                CR_OFFSET, (CR_RESET_MASK & ~CR_ENABLE_MASK));
        cam.reset();
    }

    {
        vdma_driver.configureWrite(timing[static_cast<int>(res)].h_active,
                                   timing[static_cast<int>(res)].v_active);
        Xil_Out32(GAMMA_BASE_ADDR, 3);     // Set Gamma correction factor to 1/1.8
        Xil_Out32(GRAYSCALE_BASE_ADDR, 0); // Set GrayScale mode to disabled
        // TODO CSI-2, D-PHY config here
        cam.init();
    }

    {
        vdma_driver.enableWrite();
        MIPI_CSI_2_RX_mWriteReg(XPAR_MIPI_CSI_2_RX_0_S_AXI_LITE_BASEADDR,
                                CR_OFFSET, CR_ENABLE_MASK);
        MIPI_D_PHY_RX_mWriteReg(XPAR_MIPI_D_PHY_RX_0_S_AXI_LITE_BASEADDR,
                                CR_OFFSET, CR_ENABLE_MASK);
        cam.set_mode(mode);
        cam.set_awb(OV5640_cfg::awb_t::AWB_ADVANCED);
    }

    // Bring up output pipeline back-to-front
    {
        vid.reset();
        vdma_driver.resetRead();
    }

    {
        vid.configure(res);
        vdma_driver.configureRead(timing[static_cast<int>(res)].h_active,
                                  timing[static_cast<int>(res)].v_active);
    }

    {
        vid.enable();
        vdma_driver.enableRead();
    }
}
```

### 解説1(PS部)
pipeline_mode_change()関数にグレースケールレジスタ制御を追加しただけになります。


```c++
        case 'i':
            xil_printf(
                "  Please press the key corresponding to the desired GrayScale "
                "change:\r\n");
            xil_printf("    1. Enable GrayScale Mode\r\n");
            xil_printf("    2. Disable GrayScale Mode\r\n");
            read_char1 = getchar();
            getchar();
            xil_printf("Read: %d\r\n", read_char1);
            switch (read_char1)
            {
            case '1':
                Xil_Out32(GRAYSCALE_BASE_ADDR, 1);
                xil_printf("Enabled GrayScale Mode\r\n");
                break;
            case '2':
                Xil_Out32(GRAYSCALE_BASE_ADDR, 0);
                xil_printf("Disabled GrayScale Mode\r\n");
                break;

            default:
                xil_printf(
                    "  Selection is outside the available options! Please "
                    "retry...\r\n");
            }
        default:
            xil_printf(
                "\r\n  Selection is outside the available options! Please "
                "retry...");
```

### 解説2(PS部)
main関数のswitch文に”**i**”条件を追加しただけになります。

# 実機動作
![pcam5c_grayscale.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3290770/4b39aa23-926f-4db1-bdcd-9a099a17ff4b.gif)

# 最後に
今回は以上になります。

グレースケール化のみの追加でしたので、然程難しくはなかったと思います。
次はHLSを絡ませたり、ソフト側でなにかをやりたいと思っています。