#はじめに
以下の3点を読んでから記事を読んでください。
- この記事は,私の経験からなる勘違いで構成されております
- 私の考えたRTL記述のためバグが含まれている可能性がありuse case以外では動作しない場合があります
- ZYBOを使用した実装例を記載していますがI/Oが死んだとしても責任を負いかねます
OV7670は大学でCyclone Vに実装したことがありましたが、その中身はCQ出版の本を参考に実装しただけなので実際は良く分からず設計していました。社会人になってRTL設計の方法を勉強し、雰囲気を掴み始めたので資料作成の練習も兼ねてOV7670をやり直しました。
#準備
環境
- Vivado 2016.2
- VGA対応ディスプレイ
- Windows 10(関係ない)
##使用物
- ZYBO
- aitendo OV7670カメラモジュール
- PMODとOV7670との接続出来る何か
#SCCB IF設計
SCCB IFはI2Cの下位互換の通信規格みたいなものなのでCPUからI2Cでレジスタ設定出来ますが今回は書き込みのみで簡単なのでRTLで設計しました。
##方針
- 電源投入時にレジスタの初期化するだけ動作する回路
- 入出力は最小限にして独立して組み込めるようにする
- CPUや専用IPを使用せずボード依存のないRTL記述
- レジスタ設定は簡単に出来る
##SCCB IF
SCCB Iのタイミングチャートを図1に載せます。
図1. ID=0x42 ADDRESS=0x12 DATA=0x80を送信するタイミングチャート(クリックして拡大)
読み出し機構なし、ACK無視でこの波形が出力される回路ならOKってことで設計する。
図2.30bitシフトレジスタ
図1の波形を出力するだけなら30bitのレジスタを用意して出力するデータ全部セットしてMSBから抜いていくのが簡単だと思います。SCLは、200[kHz]のクロックの反転で生成してデータに合わせてマスクして出力します。
##ACK対策
先人のブログやなどを参考にしたところACK時はHigh出力にしている方が多かったので自分もそう設計したのですが、なぜいいのか私は分かっていません(誰か知っていたら教えて欲しい)。
図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の機能ブロック図はこんな感じになりました。
図4. sccb ifの機能ブロック図
- ROM BlockはOV7670に転送するアドレスとデータを記述
- STATEはリセット後の電源安定待ちや転送データセットや転送などの状態を管理
- TIMERは転送クロックのカウントや電源安定待ち時間生成のためのブロック
- ADDRは転送1回終了ごとにアドレスインクリメント
以下にコードと使い方を貼ります。
##sccb_if.v
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
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出版のこの本で紹介されている設定値を参考にすると出来ると思います。他のフォーマットは他の先人のブログを参考にすれば出来ると思います。
#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の機能ブロック図は図6になりました。(topモジュールではないので詳しくはサンプルプロジェクトを見てください)。
図6. camera controllerの機能ブロック図
- 入力クロック50[MHz]から25[MHz]を生成してXCLKと各ブロックへ接続
- RGB565 Ctrlで8bitから16bitに変換してSRAMに書き込み
- HREFとVSYNCから書き込みアドレスと有効信号生成
- VGA Geeneratorで25[MHz]からVGAのタイミングを生成
- VGAのタイミングに合わせてSRAMからデータを読み出し
#サンプルプロジェクト
これ以上コードを貼るとすごい行になり始めるのでGitHubにプロジェクトごと置いときます。
READMEにピン配置置いておきます。
良く分からない所はソースコード読んでください(説明が中途半端ですみません)。
sccb_rom.vの設定値をちゃんと設定すると画像出力は出来ると思います。
図7. 自作したプロジェクトの画像
#おわりに
今年はプロジェクトの土台作っただけで終わってしまいましたが、年始くらいにこのプロジェクトからカメラ部分をAXI-Stream IPに作り直してDMAでSDRAMに転送出来るようにしようと思います。OSから高速にデータを参照出来るようになりやっとZYNQを使う意義が出てきます。最近のFPGA界では高位合成とかDeep Learningが流行ってる中まだVivadoを使い方の勉強をしているので来年は追いつけるよう頑張ります。
#参考資料
- aitendo OV7670
- ZYBO
- Block Memory Generator