OV7670をやり直した


#はじめに
以下の3点を読んでから記事を読んでください。

- この記事は,私の経験からなる勘違いで構成されております
- 私の考えたRTL記述のためバグが含まれている可能性がありuse case以外では動作しない場合があります
- ZYBOを使用した実装例を記載していますがI/Oが死んだとしても責任を負いかねます

OV7670は大学でCyclone Vに実装したことがありましたが、その中身はCQ出版の本を参考に実装しただけなので実際は良く分からず設計していました。社会人になってRTL設計の方法を勉強し、雰囲気を掴み始めたので資料作成の練習も兼ねてOV7670をやり直しました。


#準備
## 環境

- Vivado 2016.2
- VGA対応ディスプレイ
- Windows 10(関係ない)

##使用物
- [ZYBO](http://akizukidenshi.com/catalog/g/gM-07740/)
- [aitendo OV7670](http://www.aitendo.com/product/1784)カメラモジュール
- PMODとOV7670との接続出来る何か
  

#SCCB IF設計
SCCB IFはI2Cの下位互換の通信規格みたいなものなのでCPUからI2Cでレジスタ設定出来ますが今回は書き込みのみで簡単なのでRTLで設計しました。

##方針
- 電源投入時にレジスタの**初期化するだけ**動作する回路
- 入出力は最小限にして**独立して組み込める**ようにする
-  CPUや専用IPを使用せず**ボード依存のない**RTL記述
- レジスタ設定は**簡単**に出来る


##SCCB IF
SCCB Iのタイミングチャートを図１に載せます。
![ShiftReg.png](https://qiita-image-store.s3.amazonaws.com/0/97833/4df4855a-73a7-51af-0571-a501bcc8c08c.png)
図1. ID=0x42 ADDRESS=0x12 DATA=0x80を送信するタイミングチャート(クリックして拡大)
読み出し機構なし、ACK無視でこの波形が出力される回路ならOKってことで設計する。
![shiftreg_img.png](https://qiita-image-store.s3.amazonaws.com/0/97833/928f482f-a953-3fd0-9be1-b4d2f6c7e123.png)

図2．30bitシフトレジスタ
図1の波形を出力するだけなら30bitのレジスタを用意して出力するデータ全部セットしてMSBから抜いていくのが簡単だと思います。SCLは、200[kHz]のクロックの反転で生成してデータに合わせてマスクして出力します。

##ACK対策
先人のブログやなどを参考にしたところACK時はHigh出力にしている方が多かったので自分もそう設計したのですが、なぜいいのか私は分かっていません(誰か知っていたら教えて欲しい)。
![ack.png](https://qiita-image-store.s3.amazonaws.com/0/97833/1f790456-c4e3-c81e-e3b1-f3d53a4b32ab.png)
図3．ACKの回路動作
図のような動作ならLかZにしないと良くない気がしているのと、そもそもカメラモジュールは2.8[V]でプルアップしているのと出力電圧3.3[V]の違いは良いのかも分かっていません(入力定格3[v])。SCCBのデータシート見た中に抵抗を通信線の間に入れている図があり「Conflict-protection Resistors」って記載されているので今回は**JEポート**を使ってACK時は**High出力**にしました。結果的に動作しているからOKと思いますが良く分かっていません。

**追記**：ACKのタイミングでLow出力でも一様動作しました。


##SCCB IFブロック図
今回設計したSCCB IFの機能ブロック図はこんな感じになりました。

![sccb_if.png](https://qiita-image-store.s3.amazonaws.com/0/97833/b7fb0480-c6b5-f40c-42a6-7eaa7dc115f4.png)

図4. sccb ifの機能ブロック図

- ROM BlockはOV7670に転送するアドレスとデータを記述
- STATEはリセット後の電源安定待ちや転送データセットや転送などの状態を管理
- TIMERは転送クロックのカウントや電源安定待ち時間生成のためのブロック
- ADDRは転送1回終了ごとにアドレスインクリメント


以下にコードと使い方を貼ります。
##sccb_if.v

```verilog

module sccb_if(
input clk_25,
input rst,
output sda,
output scl
);

parameter SendCnt = 8'd98;

parameter IdAddr = 8'h42;

parameter Start = 4'h0;
parameter WaitPowerOn = 4'h1;
parameter DataSet = 4'h2;
parameter DataSend = 4'h3;
parameter AddrAdd = 4'h4;
parameter Wait = 4'h5;
parameter End = 4'h6;

parameter TimerOn = 1'b1;
parameter TimerOff = 1'b0;

reg timer;
reg [9:0] div_clk;
reg [3:0] state;
reg [7:0] cnt;
reg [29:0] shift_reg;
reg [7:0] rom_addr;

wire scl_n;
wire [15:0] rom_data;
wire [29:0] send_data;

assign send_data = {2'b0,IdAddr,1'b1,rom_data[15:8],1'b1,rom_data[7:0],1'b1,1'b0};

//div clk genearte
always @(posedge clk_25 or posedge rst) begin
  if(rst) begin
    div_clk <= 10'h0;
  end else begin
    div_clk <= div_clk + 10'h1;
  end
end

//200kHz
assign scl_n = div_clk[7];

//STATE
always @(posedge scl_n or posedge rst) begin
  if(rst) begin
    state <= Start;
    timer <= TimerOff;
  end else if(state==Start) begin
    state <= WaitPowerOn;
    timer <= TimerOn;
  end else if(state==WaitPowerOn) begin
    if(cnt==SendCnt) begin
      state <= DataSet;
    end else begin
      timer <= TimerOff;
    end
  end else if(state==DataSet) begin
    state <= DataSend;
    timer <= TimerOn;
  end else if(state==DataSend) begin
    if(cnt==8'd28) begin
      state <= AddrAdd;
    end else begin
      timer <= TimerOff;
    end
  end else if(state==AddrAdd) begin
    if(rom_addr==8'd99) begin
      state <= End;
    end else begin
      state <= Wait;
      timer <= TimerOn;
    end
  end else if(state==Wait) begin
    if(cnt==8'h40) begin
        state <= DataSet;
    end else begin
      timer <= TimerOff;
    end
  end
end

//TIMER
always @(posedge scl_n or posedge rst) begin
  if(rst) begin
    cnt <= 8'h0;
  end else if(timer==TimerOn) begin
    cnt <= 8'h0;
  end else if(cnt==8'hff) begin
    cnt <= cnt;
  end else begin
    cnt <= cnt + 8'h1;
  end
end

//DATA SET
always @(posedge scl_n or posedge rst) begin
  if(rst) begin
    shift_reg <= 29'h0;
  end else if(state==DataSet) begin
    shift_reg <= send_data;
  end else if(state==DataSend)begin
    shift_reg <= {shift_reg[28:0],1'b0};
  end
end

//ADDRESS INCREMENT
always @(posedge scl_n or posedge rst) begin
  if(rst) begin
    rom_addr <= 8'h0;
  end else if(rom_addr==8'hff) begin
    rom_addr <= rom_addr;
  end else if(state==AddrAdd) begin
    rom_addr <= rom_addr + 8'h1;
  end
end


assign sda = (state==DataSend && (cnt < 8'd30)) ? shift_reg[29] : 1'b1;
assign scl = (state==DataSend && (8'd1 <= cnt) && (cnt < 8'd29)) ? ~scl_n : 1'b1;


sccb_rom sccb_rom_inst(
    .clk(scl_n),
    .rst(rst),
    .addr(rom_addr),
    .data(rom_data)
);


endmodule

```
これがこのブロックのトップモジュールになります。clkには25[MHz]を入力して、リセットはHighアクティブでsclとsdaはSCCBの出力なので接続します。parameterのSendCntは設定レジスタの回数を入力するとデータ回数分動作して止まります。

##sccb_rom.v

```verilog
module sccb_rom(
input             clk,
input             rst,
input       [7:0] addr,
output reg [15:0]data 
);

always @(posedge clk or posedge rst) begin
    if(rst) begin
        data <= 16'h0;
    end else begin
        case(addr)
            //send num : data <= 16bit {address,data}
            8'd0 : data <= 16'h1280;   //register reset command
            8'd1 : data <= 16'hXXXX;
            8'd2 : data <= 16'hXXXX;
            //
            //途中省略
            //
            8'd96 : data <= 16'hXXXX;
            8'd97 : data <= 16'hXXXX;
            8'd98 : data <= 16'hXXXX;
            default : data <= 16'hXXXX;
        endcase
    end
end

endmodule
```
case文にアドレスとデータをセットにした16bitデータをdataに代入するように記述します。address=0x12 data=0x80ならdata <= 16'h1280と記述する。RGB565の出力フォーマットで処理したい方はCQ出版の[この本](http://urx.blue/AgP8)で紹介されている設定値を参考にすると出来ると思います。他のフォーマットは他の先人のブログを参考にすれば出来ると思います。

#CAMERA CONTROLLER設計

##方針

私の怠慢によりリソースの無駄遣い仕様でXilinxのIPを使用しています。

- 320(横)x240(縦)x2(RGB Data)[Byte]のDual Port SRAMを使用して入力と出力のタイミングを無視
- 入力段は8bitで入力される画素データを16bitに変換して320x240に間引いてSRAMに格納
- 出力段はVGAのタイミングに合わせて640x480にデータを引き伸ばして出力

※Dual Port SRAMで気を付ける**アドレスの衝突問題**ですがXilinxのリファレンスによると**Write Firstモード**推奨って記述されており、アドレスが衝突しても**SRAMの物理破壊は起きない**らしいので画像を出力するだけなら問題ないと思います。

##CAMERA CONTROLLERブロック図
今回自作したcamera controllerの機能ブロック図は図６になりました。(topモジュールではないので詳しくはサンプルプロジェクトを見てください)。
![camera_controller.png](https://qiita-image-store.s3.amazonaws.com/0/97833/5bbe2413-9179-e385-9fe6-1708c7dd00d3.png)
図6. camera controllerの機能ブロック図

- 入力クロック50[MHz]から25[MHz]を生成してXCLKと各ブロックへ接続
- RGB565 Ctrlで8bitから16bitに変換してSRAMに書き込み
- HREFとVSYNCから書き込みアドレスと有効信号生成
- VGA Geeneratorで25[MHz]からVGAのタイミングを生成
- VGAのタイミングに合わせてSRAMからデータを読み出し

#サンプルプロジェクト

これ以上コードを貼るとすごい行になり始めるので[GitHubにプロジェクト](https://github.com/kkumt93/ov7670_zybo)ごと置いときます。
READMEにピン配置置いておきます。
良く分からない所はソースコード読んでください(説明が中途半端ですみません)。
sccb_rom.vの設定値をちゃんと設定すると画像出力は出来ると思います。
![IMG5.jpg](https://qiita-image-store.s3.amazonaws.com/0/97833/b7554a9d-3986-54ef-7920-d0c3d35607a4.jpeg)
図7. 自作したプロジェクトの画像

#おわりに

今年はプロジェクトの土台作っただけで終わってしまいましたが、年始くらいにこのプロジェクトからカメラ部分をAXI-Stream IPに作り直してDMAでSDRAMに転送出来るようにしようと思います。OSから高速にデータを参照出来るようになりやっとZYNQを使う意義が出てきます。最近のFPGA界では**高位合成**とか**Deep Learning**が流行ってる中まだVivadoを使い方の勉強をしているので来年は追いつけるよう頑張ります。

#参考資料

- aitendo [OV7670](http://www.aitendo.com/product/1784)
- [ZYBO](http://akizukidenshi.com/catalog/g/gM-07740/)
- [Block Memory Generator](https://japan.xilinx.com/support/documentation/user_guides/j_ug473_7Series_Memory_Resources.pdf)