NSLで回路を作ろう#06: カメラキャプチャ画像をディスプレイに表示 (1)



[【改訂2版】FPGAボードで学ぶ 組込みシステム開発入門［Intel FPGA編］][text]の第9章で扱われているカメラキャプチャ回路をハードウェア記述言語のNSLで作ってみよう。

# 回路について

要は[前回][prev]の拡張というか応用というか、そのような回路
- CMOSカメラ(OV7670)から入力されてくる画像データをSDRAMに格納する。
- 格納した画像をVGAのディスプレイに出力する。

## 全体のイメージ図
とりあえず何を使うかイメージをつかむためブロック図を作成
![s-CB_0261_20240112.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/df357afe-c14a-9936-b808-e6c1a08f1424.png)


今回作成する回路：<font color="#ff7f00">橙枠</font>で示した部分
  - SCCBコントローラ回路(<ins>sccb</ins>) とカメラのキャプチャ回路(<ins>cap_ip</ins>)の2モジュール。

その他に必要なもの
- 矩形塗りつぶし：周辺機器
  - <font color="#ED7D31">**橙**</font>: CMOSカメラ(OV7670)、GPIOから接続する
  - <font color="#5B9BD5">**青**</font>: SDRAM(FPGAボード上のメモリ)、FPGAボード上に配置されている。
  - <font color="#70AD47">**緑**</font>: VGA(ディスプレイ)、VGA端子からケーブルを使用してディスプレイに接続する。
- <font color="#5B9BD5">青枠</font>：Platform Designerによって生成される部分
  - NiosII CPUとAvalon-MM等
  - 個人的にはインフラのイメージ　　
- <font color="#70AD47">緑枠</font>：SDRAMからVGAにグラフィック表示する回路
  - [前回][prev]で作成したものを使用する。

## ハードウェア

ハードウェアについて以下の点を考えていく
- 動作について
- CMOSカメラについて
- データの扱いについて

### 動作について
- SCCB通信でカメラの設定を行う
  - 設定する値等の管理はCPUのプログラムが担う
- 設定後カメラからデータが送られてくる
  - 送られてきたデータは2データで1画素分なので、それに合わせて加工する
- 加工したデータをSDRAMに転送
- 保存されたデータをVGA出力でディスプレイに表示

### CMOSカメラについて
- 解像度はVGA(`640 x 480`)にする。
- 初期の設定ではカメラは`YUV422`形式で画像データが送られるため、SCCB通信を用いてカメラの設定を行う必要がある。
  - 設定により、RGBが出力される`RGB444`形式で出力されるようにする。
  - `RGB888`で出力する方法はないようで、フルカラーの画像を取得するには`YUV422`出力を受け取って変換する必要があるようだ。
    - 4データ(8x4=32bit)から2画素に変換する(2x3x8=48bit)
    - `Y0`, `U0`, `Y1`, `V0` が入力されるので、`Y0`, `U0`, `V0`から`R0`, `G0`, `B0`、`Y1`, `U0`, `V0`から`R1`, `G1`, `B1`　に変換する

### データの扱いについて
- データ転送Avalon-MM のバースト転送を用いる。
- 画素データは1画素につき4Byteに使う。(グラフィック表示回路とbitの扱い方は同じ)
- 転送先アドレスの設定やキャプチャのオンオフ制御は、VGA表示回路と同様にPIOモジュールを使い、最上位層でpinを接続する。


## ソフトウェア

ソフトウェア側では以下のCプログラムを作成する
- キャプチャした画像をVGAに表示する
  - 準備
  - OV7670の設定・初期化
  - 各ブランクフラグを読み取る関数
  - キャプチャ回路とディスプレイ回路の設定を行う。

# 作ってみよう

今回の回路作成では以下のように考えていく。
- SCCBコントローラ回路
- カメラキャプチャ回路 
- NiosIIシステムの構築
- 最上位階層の作成
- ソフトウェアプログラムの作成

流石にボリュームが大きいため、<ins>今回は複数回に分割する </ins>
- 1 回路について、SCCBコントローラ回路(<ins>今回</ins>)
- 2 カメラキャプチャ回路
- 3 ソフトウェアプログラム&動作確認

# SCCBコントローラ回路

回路のイメージ図:
![sccb-img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/c19eee9d-c40a-f5ac-d3fa-f550cd4fa0f0.png)

CMOSカメラの使用するには設定レジスタの値を編集してを設定を行う必要がある
- 値の編集はSCCB通信で行う必要がある
- 以降でSCCB通信を制御する回路を作っていく。

## SCCB通信について

SCCB (Serial Camera Control Bus) 通信はI2Cによく似た形式になっている。
- 扱う信号線の名前が SCL, SDA という点とか。
 
Avalon-MMのようにSCCBの接続もマスターとスレーブの2系統に分けられる。
- 今回の場合、FPGAボード側をマスター、CMOSカメラ側をスレーブとして接続する。
- スレーブ回路は複数存在させられるので、1つのマスターでアドレスを指定して複数のスレーブにデータをやり取りできる。

送信するデータは以下の表のようになっており、上位bitから送信する。

SCCB送信概略:
![sccb_summary.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/f62abc69-9b1a-44f9-0159-7117589e4893.png)

データは8bitで区切られておりそれぞれの扱いとしては以下の通り
- ID Address: デバイス(CMOSカメラ) 自身のアドレス
  - 上位7bitがアドレスで、下位1bitで書込みなのか読込みなのかを指定する。
    - 書込み:`0`
    - 読込み:`1`
  - この回路では`0x42`の固定値とする
- sub-address: レジスタアドレス
  - カメラ内のどの設定レジスタの値を編集したいかアドレスを出力する。
  - Avalon-MMマスターから入力された値(REGADDR)を使う
- write data: 読み書きするデータ
  - 設定レジスタに何の値を入れるか出力する
  - Avalon-MMマスターから入力された値(REGDATA)を使う

また、8bitの後ろに1bit分だけSDA端子をハイインピーダンス(z)にする必要がある[^1]。

[^1]: I2Cではこのzの区間でACKのやり取りを行うが、SCCBではその実装が行われていないらしい。(<a href="https://sinby.com/posts/2023/09020.html">参考</a>)

SCCB通信の動作
- スタートbitの送信
  - SCLが1の状態でSDAが1から0に変化するとスタートbitとして扱われ、データの送信を開始することを意味する。
- データの送信
  - 表に示した<ins>上位bitから</ins>、以下の順番で、データを送信する。
    - ID Address
    - ハイインピーダンス
    - sub-address
    - ハイインピーダンス
    - write data
    - ハイインピーダンス
  - データの送信時ではSCLが0の時にbitの値を変化させる必要がある。
- ストップbitの送信
  - データの送信終了を示すため、SCLが1の状態でSDAを0から1に変化させ、送信終了


タイミング図にするとこんな感じ：
![sccb_timing.05.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/dd82fcc8-a48a-934f-eefc-a99bd660776e.png)

## Avalon-MMとのやり取り

データ書き込み用に Avalon-MMのスレーブ回路を実装する。
- 各アドレス、bitの扱いは以下の表に従う

![sccb_avs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/b64cf3f0-eba1-1940-a33b-df261557dcba.png)

それぞれのbitの扱いは以下の通り。
- REGADDR
  - SCCBに送信するsub-addressの値
- REGDATA
  - SCCBに送信するwrite dataの値
- BUSY
  - 1であるときデータ送信中である事を示す。
  - プログラムは、この信号が0であることを確認してからREGADDR, REGDATAの書込みを行う。

## 機能分割

SCCBコントローラ回路の機能をまずは大きく3つに分けてみる。

ブロック図:
![sccb_fn_summary.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/d924a020-ae3b-75bf-028f-093f6628db3e.png)

それぞれの機能について
- [1] Avalon-MM Slave インターフェース
  - Avalon-MM(Memory Mapped)からの信号を処理する
- [2] パラレル-シリアル変換回路
  - [1]Avalon-MM Slave インターフェースから入力されるパラレル信号を、SCCB通信のシリアル信号に変換する。
- [3] 制御部
  - ステートマシンとカウンタを用い、回路の制御を行う
  - データ送信時に、通信レート(10μs)の周期のクロック信号(SCL)を出力する

以降でそれぞれの中身を考えていく。

# [1] Avalon-MM Slave インターフェース

概要: 
- Avalon-MMからの信号を処理する
- 書込み信号が来たとき
  - [2]パラレル-シリアル変換や[3]制御部にそれぞれ信号を出力する。
- 読込み要求が来たとき
  - [3]制御部から入力されるBUSY信号の値を出力する。

ブロック図:
![sccb_fn_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/d64da8e2-5d1b-1b8d-888b-26b8c1547e81.png)

図の中で分けられた部分は、それぞれ以下に示す役割を担う。
- (1) データ書込み
  - Avalon-MM：addressがSCCB送信データを指している状態でwrite信号が入力
    - [2]パラレル-シリアル変換、[3]制御部へ書込があった事を通達する信号を出力
- (2) BUSY信号返答
  - Avalon-MM：addressがBUSY信号フラグを指している状態でread信号が入力
    - BUSY信号の値を返す。

# [2] パラレル-シリアル変換回路

概要:
- [1]Avalon-MM Slave インターフェースから入力されるパラレル信号を、SCCB通信のシリアル信号に変換する。
- 動作内容
  - 入力されたデータをシフトレジスタに格納する。
  - [3]制御部の信号を受けながらビットシフトしていく。
  - シフトレジスタの、最上位bitをSCCBのデータ信号(SDA)として出力する。
    - SDAは8bit送信する毎に10μsのハイインピーダンス(Z)にする。

ブロック図:
![sccb_fn_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/84643a1f-7392-2d8b-e9b3-3ffe89a44ef4.png)

図の中で分けられた部分は、それぞれ以下に示す役割を担う。
- (1) 通信データ用シフトレジスタ
  - [1] Avalon-MM Slave インターフェースから送られてきたデータを格納
  - 最上位bitから順番に送信
  - 制御部から送られてくる信号にあわせ、bitシフトを行う。
    - 順番にSDAに出力していく。
- (2) Z送信カウンタ
  - bitシフト信号に合わせてカウントアップしていくカウンタ
  - 8bit送信後1bitだけZを送信するタイミングで制御信号を生成
    - SDAの出力をZにする。
- (3) SDA出力生成
  - (1) 通信データ用シフトレジスタの最上位bitを取得しSDAに出力
  - (2) Z送信カウンタから制御信号が入力されている場合、Zを出力


# [3] 制御部

概要:
- ステートマシンとカウンタを用い、回路の制御を行う。
  - ステートマシン:状態制御
  - カウンタ:タイミング制御
- データ送信中の状態のときにSCCB通信用のクロック信号(SCL, 周期:10μs)を生成・出力する。

ブロック図:
![sccb_fn_03_v01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/9f9eabf5-6613-669c-2d4a-7d78284033cf.png)
- 図中：<font color="#70AD47">緑線</font>…状態信号、<font color="#5B9BD5">青線</font>…タイミング制御信号
  - 他の信号より影響度が大きいため色分けしている。

図の中で分けられた部分は、それぞれ以下に示す役割を担う。

- (1) BUSY信号
  - 現在SCCB通信が行われ、書込み不可状態か否かを示す信号を生成
  - 書込み検知の信号(write_detect)が入力されたとき
    - (7)SCCB通信開始からbusy_set1信号を受取りBUSY信号の値を1にする
  - 送信終了後、一定時間経過で(3)200μs遅延カウンタからbusy_set0信号が入力
    - BUSY信号の値を0にする。
- (2) データ送信カウンタ
  - ステートが送信状態(SENDING)の間
    - 送信されたbit数を数えていく
  - データ送信が完了
    - ステートマシンに送信終了信号(sendend)を出力する
- (3) 200μs遅延カウンタ
  - カメラ側の仕様でSCCB通信終了後一定時間(約200μs)送信の間隔を空ける必要がある
    - その為、送信終了後の時間経過を数えていく。
  - カウンタにより一定時間時間経過後
    - ステートマシンに遅延時間終了(busyend)を入力
    - busy_set0信号を(1)BUSY信号に出力
- (4) SCL生成
  - ステートマシンが送信状態(SENDING)である間
    - 送信レートに合わせたクロック信号をタイミング制御のカウンタから値を参照し、生成する。
- (5) タイミング制御
  - 送信レートの周期(10μs)で1周するカウンタを実装
  - カウンタの値を用いてタイミング制御信号を生成し。制御する。
  - 制御するタイミング
    - SCL生成
    - SDAの出力
    - ステートマシンの状態遷移
- (6) ステートマシン
  - 処理の全体的な流れを、状態遷移を用いて管理する。
    - 詳しくは後述
- (7) SCCB通信開始[^2]
  -  [1]からの書込み検知信号が入力されると以下を行う
     - SCCB通信開始を示す信号をステートマシンに出力
     - (1)BUSY信号にbusy_set1信号を出力

[^2]: この部分は設計中に追加した部分なので番号が後ろになってしまった。

## ステートマシンの動作について
状態の一覧:
![sccb_state_summary.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/e6849862-33a9-da83-5de5-fb0d4d4392f1.png)

各状態の説明及び動作の流れ
- HALT
  - 初期状態
  - シフトレジスタにデータが書込まれるまで待機
  - データの書込みを検知したらSTARTへ遷移
  - STARTへ遷移するタイミングでデータ受付不可にする。
    - BUSY信号オン
- START
  - 書込まれたデータを送信するため、スタートbitを出力
  - スタートbitを出力後、SCCB通信を開始してSENDINGへ遷移
- SENDING
  - シフトレジスタに書込まれたデータを上位bitから出力。
  - データは固定長。
    - 送信したbit数を数えて、規定数に達したら送信終了。
  - 送信終了時にBUSYDELAYへ遷移
- BUSYDELAY
  - 送信終了後、次のデータを受け付ける前SCCB通信の間隔を一定時間(約200μs)空ける必要
    - 今回使用するカメラ(OV7670)の仕様
  - 250μs経過後、HALTへ遷移
    - 時間経過のチェックはカウンタを用いる
  - HALTへ遷移するタイミングで、次のデータを受付可能にする。
    - BUSY信号オフ

ステートマシン図:
![sccb_state.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588818/4f1423a1-6153-b1dc-befa-3fb3bc531f83.png)

# 記述ファイル

ヘッダファイル 
```nsl:sccb.nsh
#ifndef _SCCB_NSH
#define _SCCB_NSH

declare sccb {
    // Avalon MM Slave Interface
    input avs_address;
    output avs_readdata[16];
    input avs_writedata[16];

    func_in avs_read(avs_address) : avs_readdata;
    func_in avs_write(avs_address, avs_writedata);

    // SCCB 通信
    output SCL;
    output SDA;
}

#define CNTMAX 1000

#endif // _SCCB_NSH

```

モジュール
```nsl:sccb.nsl
#include "sccb.nsh"

module sccb {
    // state
    state_name HALT, START, SENDING, BUSYDELAY;
    
	// func
    func_self reg_write();
    func_self write_detect();

    func_self z_en();

    func_self busy_set1();
    func_self busy_set0();

    func_self en_10us();
    func_self sft_en();
    func_self scl_set1();
    func_self scl_set0();

    func_self send_end();
    func_self busy_end();

    // reg
    reg sccb_busy = 0;
    
    reg sft_sda[30] = 30'h3fffffff;
    reg z_cnt[4] = 0;

    reg sccb_start = 0;
    reg timing_cnt[10] = 0;
    
    reg send_cnt[5] = 0;
    
    reg busy_cnt[5] = 0;

    reg SCL_r = 1;
    // wire

    // always ---------------------

    if (en_10us == 1) timing_cnt := 0;
    else timing_cnt++;
    
    if (timing_cnt == (CNTMAX - 1)){
        en_10us();
    }

    // funcs  ---------------------
    // in   ---------------------

    func avs_read {
        return if(avs_address == 1) {15'h0, sccb_busy} else 16'h0;
    }

    func avs_write {
        if (avs_address == 0 && sccb_busy == 0) {
            reg_write();
            write_detect();
        }
    }

    // self ---------------------
    func reg_write {
        sft_sda := {1'b1, 1'b0, 8'h42, 1'b0, 
        avs_writedata[15:8], 1'b0, avs_writedata[7:0], 1'b0, 1'b0};
    }

    func write_detect {
        busy_set1();
        sccb_start := 1;
    }

    func busy_set1 {
        sccb_busy := 1;
    }
    func busy_set0 {
        sccb_busy := 0;
    }

    func sft_en {
        sft_sda := {sft_sda[28:0], 1'b1};
    }

    func scl_set0 {
        SCL_r := 0;
    }

    func scl_set1 {
        SCL_r := 1;
    }

    // state blocks ---------------

    state HALT {
        z_cnt := 0;
        if(sccb_start && en_10us) {
            sccb_start := 0;
            goto START;
        }
    }

    state START {
        if(en_10us){
            goto SENDING;
        }
        if(timing_cnt == ((CNTMAX / 4)  - 1)) sft_en();
    }

    state SENDING {
        if(en_10us){
            if( send_end == 1 ){
                send_cnt := 0;
                goto BUSYDELAY;
            }
            else send_cnt++;
        }
        if(sft_en){
            if (z_en == 1){ 
                z_cnt := 1;  
            }else z_cnt++;
        }
        if(z_cnt == 9)z_en();
        if(send_cnt == 27) send_end();

        if(timing_cnt == ((CNTMAX / 4)  - 1)) sft_en();

        if(timing_cnt == 2) scl_set0();
        if(timing_cnt == ((CNTMAX / 2)  + 2)) scl_set1();
    }

    state BUSYDELAY {
        func en_10us{
            if(busy_end == 1){
                busy_set0();
                busy_cnt := 0;
                goto HALT;
            }
            else busy_cnt++;
        }
        if(busy_cnt == 19) busy_end();
        if(timing_cnt == ((CNTMAX / 4)  - 1)) sft_en();
    }

    SCL = SCL_r;
    SDA = if(z_en == 1) 1'bz else sft_sda[29];
}

```

# まとめ
今回はカメラキャプチャ回路の概要とその内SCCB通信回路の部分を作成した。
- システムのイメージ
- 構成する要素を考えた
  - SCCB通信回路
  - カメラキャプチャ回路
  - C言語のソフトウェアプログラム
- SCCB通信回路の作成
  - Avalon-MMからの書込みや読込みを処理する
  - 書込まれたパラレル信号をシリアル信号に変換して出力
  - ステートマシンとカウンタを使って通信の制御を行う

次回はカメラキャプチャ回路を作成する

# 参考文献

- [【改訂2版】FPGAボードで学ぶ 組込みシステム開発入門［Intel FPGA編］][text]

[text]: https://gihyo.jp/book/2018/978-4-7741-9388-5
[prev5]: https://qiita.com/mk_masa/items/2c4eb17dde97695cee4c
[prev4]: https://qiita.com/mk_masa/items/85d6e47b081195c277db
[prev3]: https://qiita.com/mk_masa/items/2088aac1ca16b49df952
[prev2]: https://qiita.com/mk_masa/items/fc846bf6e4472593900e
[prev]: https://qiita.com/mk_masa/items/b725323a9844bf882975