01signal.com

Smart Zynq で OV7670 カメラセンサーのライブビューとビデオキャプチャ

この Web ページは、Smart Zynq ボードの機能を探求する小規模プロジェクトのグループに属しています。

このプロジェクトは、HelloFPGA にも掲載されています。中国語の読者にはそちらをおすすめします。

はじめに

このチュートリアルでは、OV7670 カメラモジュールを Smart Zynq に接続し、ボード上の HDMI 出力でライブビデオ信号を表示する方法を説明します。また、簡単な Linux コマンドを使って、生のビデオストリームをファイルに保存する方法も紹介します。

このチュートリアルの内容は、他のタイプのデータ収集 (data acquisition) にも当てはまります。以下で紹介する手法は、他の画像データソースや、その他のデジタルデータソースでも利用できます。

カメラモジュールは Zynq チップの PL (FPGA) 部分に接続されています。そのため、画像データを ARM プロセッサに送る前に、画像処理を実装するロジックを追加するのは簡単です。このチュートリアルは Xillinux を基にしているので、システムのプロセッサ部分は完全な Linux ディストリビューションで構成されています。

このデモでは OV7670 モジュールを選びました。このハードウェアは安価で人気があり、入手も簡単だからです。さらに、この部品が生成するデジタル信号は理解しやすいという利点もあります。

ただし、OV7670 には残念な欠点があります。デフォルトではビデオストリームの色が正しくありません。これはこのカメラセンサーでよく知られている問題です。この欠点は、カメラの少数のレジスタを変更することで修正できます。そのため、このチュートリアルは 2 つの部分に分かれています。

このチュートリアルの大部分は実装の仕組みの説明です。カメラを使うだけなら、これらの説明を理解する必要はありません。

OV7670 モジュール

このチュートリアルでは、下の写真に示すカメラモジュールを使用します。

The OV7670 camera sensor module used in this project

このモジュールでは、ピンヘッダのほとんどのピンが OV7670 部品に直接接続されています。3.3V と GND だけが電圧レギュレータに接続されています。したがって、FPGA とモジュールの間の接続は、FPGA と OV7670 部品の直接接続になります。

同じ機能を持つモジュールが市場には他にもあります。これらのモジュールでもおそらく問題ありません。たとえば、PCB に「2017/3/15」と書かれている別のモジュールがありますが、これも正しく動作します。一方、正常に動作しないモジュールもあります。動作しないモジュールは PCB に「QYF-OV7670 V3.0」と書かれています。

もう 1 つ注意すべき点は、OV7670 部品には異なるリビジョンがあることです。モジュールに正しいリビジョンが使われているかどうかを確認できます。その方法は、このチュートリアルの第 2 部で説明します。

OV7670 に関する情報源は主に 2 つあります。これらの文書はインターネットで入手できます。

Vivado プロジェクトの準備

demo バンドル (demo bundle) の zip ファイル(boot partition kit)から新しい Vivado プロジェクトを作成します。verilog/src/xillydemo.v をテキストエディタで開きます。コードの中で「PART 2」というラベルの付いた部分を削除し、その部分の代わりに次のコードスニペットを挿入します。

   /*
    * PART 2
    * ======
    *
    * This code demonstrates a frame grabber (data acquisition) from
    * an OV7670 camera module.
    *
    */

   reg [1:0]  clkdiv;

   always @(posedge bus_clk)
     clkdiv <= clkdiv + 1;

   assign J6[10] = clkdiv[1]; // MCLK / XCLK

   assign J6[0] = 0; // PWDN, the camera is always on
   assign J6[1] = !user_w_write_32_open; // RESET#, active low

   wire [7:0] D_in;
   wire       pclk_in, hsync_in, vsync_in;

   assign D_in = J6[9:2];
   assign pclk_in = J6[11];
   assign hsync_in = J6[12];
   assign vsync_in = J6[13];

   (* IOB = "TRUE" *) reg [7:0] D_guard;
   (* IOB = "TRUE" *) reg       pclk_guard, hsync_guard, vsync_guard;

   reg [7:0]  D;
   reg 	      pclk, hsync, vsync;

   always @(posedge bus_clk)
     begin
	// Metastability guards on asynchronous inputs
	D_guard <= D_in;
	pclk_guard <= pclk_in;
	hsync_guard <= hsync_in;
	vsync_guard <= vsync_in;

	D <= D_guard;
	pclk <= pclk_guard;
	hsync <= hsync_guard;
	vsync <= vsync_guard;
     end

   wire       sample_valid;
   reg 	      previous_pclk;

   always @(posedge bus_clk)
     previous_pclk <= pclk;

   assign sample_valid = pclk && !previous_pclk;

   // wait_for_frame's purpose is to start getting data from the camera
   // at the beginning of a frame.
   reg 	      wait_for_frame;

   always @(posedge bus_clk)
     if (!user_r_read_32_open)
       wait_for_frame <= 1;
     else if (sample_valid && vsync)
       wait_for_frame <= 0;

   // fifo_has_been_full changes to '1' when the FIFO becomes full, so
   // that the data acquisition stops and an EOF is sent to the host.
   // This ensures that the data that arrives to the host is contiguous.

   reg 	      fifo_has_been_nonfull, fifo_has_been_full;
   wire       fifo_full;

   always @(posedge bus_clk)
     begin
	if (!fifo_full)
	  fifo_has_been_nonfull <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_nonfull <= 0;

	if (fifo_full && fifo_has_been_nonfull)
	  fifo_has_been_full <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_full <= 0;
     end

   assign user_r_read_32_eof = fifo_has_been_full && user_r_read_32_empty;

   // This part writes pixels from the camera to the FIFO

   reg 	      fifo_wr_en;
   reg [1:0]  byte_position;
   reg [31:0] dataword;

   always @(posedge bus_clk)
     if (wait_for_frame)
       begin
	  byte_position <= 0;
	  fifo_wr_en <= 0;
       end
     else if (sample_valid && hsync)
       begin
	  case (byte_position)
	    0: dataword[7:0] <= D;
	    1: dataword[15:8] <= D;
	    2: dataword[23:16] <= D;
	    3: dataword[31:24] <= D;
	  endcase

	  if (byte_position == 3)
	    fifo_wr_en <= !fifo_has_been_full;
	  else
	    fifo_wr_en <= 0;

	  byte_position <= byte_position + 1;
       end
     else
       fifo_wr_en <= 0;

   fifo_32x512 fifo_32
     (
      .clk(bus_clk),
      .srst(!user_r_read_32_open),

      .din(dataword),
      .wr_en(fifo_wr_en),
      .full(fifo_full),

      .rd_en(user_r_read_32_rden),
      .dout(user_r_read_32_data),
      .empty(user_r_read_32_empty)
      );

または、この変更を行った後の xillydemo.v をここからダウンロードしても構いません。

この変更を行ったら、いつもどおりビットストリーム (bitstream) ファイルを作成します。この Verilog コードがどのように動作するかは、このページの後半で詳しく説明します。

カメラモジュールの接続

カメラモジュールと Smart Zynq ボードの接続には、短い Dupont ジャンパーワイヤを使用できます。ワイヤの長さは 10 cm 以下にしてください。最適な長さは 5 cm です。ワイヤが長いと、クロストークによってデジタル信号の品質が劣化する可能性があります。水平同期信号に過度のノイズが乗ると、緑と紫の縞模様が現れて映像が飛びます。

ワイヤの長さが 10 cm の場合は、OV7670 の I/O ドライバ電流を減らすためにレジスタを変更する必要があるかもしれません。この変更方法は、このチュートリアルの第 2 部で説明します。

Smart Zynq SP ボードに接続した OV7670 モジュールの写真を以下に示します。

OV7670 module connected to the Smart Zynq board

次に、反対方向から撮影した写真を示します。左上の小さな画像は、ピンヘッダの最後のピンが何にも接続されていないことを強調しています。

OV7670 module connected to the Smart Zynq board

上の写真はワイヤの接続方法を示しています。まず、Smart Zynq ボードの裏面に「Bank 33 VCCIO Vadj」と書かれている場所を探します。この表示の近くにあるピンの列が今回使うピンヘッダです。HDMI コネクタの近くにあるピンヘッダです。

カメラセンサーと Smart Zynq ボードの間には、16 本のワイヤが並列に接続されます。そのうち 3.3V と GND の 2 本だけは、ピンヘッダ内の別の場所に接続します。この 2 本は短くする必要はありません。

ピンヘッダの最後のピンは 5V です。このピンにはワイヤを接続しないでください。

カメラモジュールと Smart Zynq のピンヘッダ間の配線仕様を以下に示します。この情報は上の写真から読み取ることもできます。

ピンヘッダ 1 3 5 7 9 11 13 15 35
モジュールのピン PWDN D0 D2 D4 D6 MCLK HS SDA GND
モジュールのピン RST D1 D3 D5 D7 PCLK VS SCL 3.3V
ピンヘッダ 2 4 6 8 10 12 14 16 37

もう一度言いますが、3.3V と GND の接続には特に注意してください。この 2 本を誤って接続すると、カメラモジュールが破損する可能性があります。

フレームキャプチャ

上記の更新済み xillydemo.v に基づくビットストリーム (bitstream) ファイルで Xillinux を起動します。

次のコマンド(シェルプロンプトで実行)は、カメラの出力から短いビデオクリップを作成します。

# cat /dev/xillybus_read_32 > clip.raw

このコマンドは数秒間実行された後、停止します。ビデオストリームのデータレートが SD カードへの書き込み速度よりも高いためです。これによりオーバーフロー (overflow) が発生し、データフローが停止します。この動作の仕組みについては後述します。

このビデオクリップは、次のコマンドで再生できます。

# mplayer -demuxer rawvideo -rawvideo w=640:h=480:format=uyvy:size=614400:fps=31.25 clip.raw

このコマンドを Xillinux のグラフィカルデスクトップ内のターミナルウィンドウで実行すると、ビデオは Xillinux のグラフィカルインターフェース上で再生されます。

別のコンピュータの画面でビデオを再生するには、別のページで説明しているテクニックを使用することもできます。たとえば、相手のコンピュータの IP アドレスが 192.168.1.11 の場合、コマンドが次のように始まるように変更します。

# DISPLAY=192.168.1.11:0 mplayer -demuxer rawvideo ...

前述のとおり、このビデオクリップの色は正しくありません。この問題の修正方法は次のページで説明します。

次のコマンドは、1 つのビデオフレームを frame.raw というファイルに読み込みます。

# dd if=/dev/xillybus_read_32 of=frame.raw bs=614400 iflag=fullblock count=1

生フレームのフォーマットは UYVY 4:2:2 です。つまり、各ピクセルは 16 ビットで構成されます。最初のバイトは先頭ピクセルの U コンポーネント (Cb) です。次のバイトは同じピクセルの Y コンポーネントです。3 番目と 4 番目のバイトは、2 番目のピクセルの V コンポーネントと Y コンポーネント (Cr と Y) です。

このファイルは次のコマンドで PNG ファイルに変換できます。

# convert -size 640x480 pal:frame.raw frame.png

画像を表示するための簡単なツールもあります。

# display frame.png &

ライブビュー

カメラのライブビューを見るには、liveview.sh というファイルに次の内容を記述して作成します。

#!/bin/bash

while [ 1 ] ; do
  dd if=/dev/xillybus_read_32 bs=614400 iflag=fullblock count=1 2>/dev/null
done | mplayer -demuxer rawvideo -rawvideo w=640:h=480:format=uyvy:size=614400 -

このスクリプトは次のコマンドで実行します。

# bash liveview.sh

なぜこのスクリプトが必要なのでしょうか。mplayer が遅すぎるため、デバイスファイルからデータを直接読み込んで再生することはできません。つまり、Zynq の ARM プロセッサは、正しいフレームレート (31.25 fps) でビデオを再生するほどの性能を持っていないのです。直接読み込みを試すと、ビデオはほんの少ししか再生されません。FPGA 内でオーバーフロー (overflow) が発生すると、データの流れが止まります。

このスクリプトは、毎回 /dev/xillybus_read_32 から 1 フレーム分の生データを読み込む無限ループに基づいています。これは先ほど 1 フレームを frame.raw に読み込むために使ったコマンドと同じです。ただし今回は dd に出力ファイルを指定していないので、dd はデータを標準出力に書き出します。

この無限ループの出力は、パイプ(ループの末尾にある「|」に注目してください)によって mplayer の標準入力にリダイレクトされます。mplayer は標準入力から届いたビデオデータを再生します。

このスクリプトがオーバーフロー (overflow) の問題を解決するのは、dd が常に完全な 1 ビデオフレームを読み込むからです。mplayer がさらに多くのデータを受け入れる準備ができていないときは、パイプのデータフローが一時的に停止します。その結果、dd はカメラセンサーからのビデオフレームをスキップします。したがって、画面に表示されるフレームレートはカメラのフレームレートより低くなります。正確に言うと、表示されるフレームレートは mplayer が表示できる最大のフレームレートです。

画面に表示されるビデオ画像には、mplayer 自身のバッファリングによるわずかな遅延があります。低遅延を実現するには、バッファを追加せずに画像を画面に表示する簡単なプログラムを書く必要があります。

mplayer は高性能なメディアプレイヤーです。たとえば、色が正しくないのが気になる場合は、ビデオクリップを白黒で再生することもできます。彩度をゼロにするには、コマンドに次の部分を追加します。

# mplayer -saturation -100 -demuxer rawvideo ...

このページの実践編の終わり

このページの残りの部分では、データ収集 (data acquisition) を行うロジックの実装について説明します。実践的な内容にだけ興味がある場合は、このチュートリアルの次のパートに進んでください。

FPGA とホストの間の通信

この例のロジックは、Xillybus IP コア (IP core) に基づいています。この IP コアはホストとの通信を担当します。

前述のように、Verilog コードで置き換えた部分は、次のコードで終わっています。

   fifo_32x512 fifo_32
     (
      .clk(bus_clk),
      .srst(!user_r_read_32_open),

      .din(dataword),
      .wr_en(fifo_wr_en),
      .full(fifo_full),

      .rd_en(user_r_read_32_rden),
      .dout(user_r_read_32_data),
      .empty(user_r_read_32_empty)
      );

これは標準的な FIFO のインスタンシエーション (instantiation) です。この FIFO には、データを読み出すためのポートが 3 つあります。rd_en、dout、empty (空) です。これらのポートは Xillybus IP コアに接続されています。これにより、IP コアは FIFO からデータを読み出してホストに送信できます。その結果、FIFO に書き込まれたものはすべて /dev/xillybus_read_32 というデバイスファイルに届きます。言い換えれば、ホスト上の通常のコンピュータプログラムは /dev/xillybus_read_32 を通常のファイルとして開くことができます。プログラムがこのファイルを読み込むと、FPGA 内のアプリケーションロジックが FIFO に書き込んだデータを受け取ります。

この FIFO には、データを書き込むためのポートが 3 つあります。wr_en、din、full (満杯) です。これらのポートは、カメラセンサーからのピクセルデータを収集するロジックに接続されています。このロジックがどのように動作するかは、このページの次のセクションで説明します。ここでは、このロジックが @dataword と @fifo_wr_en を使ってピクセルデータを FIFO に書き込むことだけを指摘しておきます。ここから先は、このデータをホスト上で動作するコンピュータプログラムに届けるのが Xillybus IP コアの役割です。だからこそ、先ほど示した次のコマンドでこのデータがファイルに書き込まれるのです。

# cat /dev/xillybus_read_32 > clip.raw

FIFO の一般的な仕組みについては、このページを参照してください。

データフローは次の図のようにまとめられます。

Simplified diagram of data acquisition with Xillybus

このウェブサイトには Xillybus に関するセクションがあり、その中にデータ収集 (data acquisition) について論じたページがあります。そのページを通読すると役立つかもしれません。

user_r_read_32_open は FIFO の srst ポートに接続されていることに注意してください。ホスト上で /dev/xillybus_read_32 が開かれると、この信号は High に変化します。この信号は NOT(否定)を介して接続されているため、デバイスファイルが閉じられると FIFO はリセット状態になります。これにより、ファイルが閉じられるたびに FIFO 内のすべてのデータが破棄されます。

カメラセンサーとのインターフェース

それでは、先ほどの Verilog コードの冒頭を見てみましょう。

   reg [1:0]  clkdiv;

   always @(posedge bus_clk)
     clkdiv <= clkdiv + 1;

   assign J6[10] = clkdiv[1]; // MCLK / XCLK

@bus_clk の周波数は 100 MHz です。このクロックは @clkdiv によって 4 分周されます。したがって、カメラモジュールには 25 MHz のリファレンスクロックが供給されます。カメラのデータシートによれば、これは許容される周波数です。ただし、カメラはリファレンスクロックが 24 MHz のとき 30 fps のビデオストリームを生成するように設計されています。実際のビデオフレームレートはそのためわずかに高く、31.25 fps になります。

ここで言っておきますが、これは通常、クロックを生成する方法としては正しくありません。正しい方法は PLL などのリソースを使うことです。ただし、この特定のケースでは問題ありません。@clkdiv は出力信号を生成するためだけに使われ、FPGA 自身のロジックはこの信号を使わないからです。

Verilog コードの次の部分はこれです。

   assign J6[0] = 0; // PWDN, the camera is always on
   assign J6[1] = !user_w_write_32_open; // RESET#, active low

J6[0] はカメラモジュールの PWDN ピンに接続されています。カメラの電源が切られることはありません。

J6[1] はカメラの RESET# に接続されています。このピンが Low のとき、カメラはリセットされます。@user_w_write_32_open は、ホスト上のプログラムが /dev/xillybus_write_32 を開くと High になります。通常は @user_w_write_32_open が Low なので J6[1] は High になり、カメラはリセットされません。この構成により、次のコマンドでカメラをリセットできます。

# echo 1 > /dev/xillybus_write_32

このコマンドはデバイスファイルを短い期間だけ開くため、望みどおりの結果が得られます。

ここまでは、FPGA からカメラセンサーへの信号の生成方法を示しました。次に、カメラセンサーから FPGA への信号を見ていきます。

OV7670 は、FPGA からのリファレンスクロックと同じ周波数のピクセルクロックを生成します。つまり、PCLK の周波数は 25 MHz です。この信号は Verilog コード内で @pclk_in に接続されています。

カメラセンサーは、ビデオデータを含む 3 つの信号も生成します。Verilog コード内のこれらの信号の名前は @D_in、@hsync_in、@vsync_in です。カメラセンサーは、@pclk_in が High から Low に変化する(立ち下がりエッジ)と同時にこれらの信号の値を変更します。より正確には、@D_in、@hsync_in、@vsync_in の変化は @pclk_in の立ち下がりエッジに合わせられています。FPGA から見た場合、これはソースシンクロナス・クロック (source synchronous clock) 入力と呼ばれます。

関連する Verilog コードの部分を見てみましょう。

   wire [7:0] D_in;
   wire       pclk_in, hsync_in, vsync_in;

   assign D_in = J6[9:2];
   assign pclk_in = J6[11];
   assign hsync_in = J6[12];
   assign vsync_in = J6[13];

   (* IOB = "TRUE" *) reg [7:0] D_guard;
   (* IOB = "TRUE" *) reg       pclk_guard, hsync_guard, vsync_guard;

   reg [7:0]  D;
   reg 	      pclk, hsync, vsync;

   always @(posedge bus_clk)
     begin
	// Metastability guards on asynchronous inputs
	D_guard <= D_in;
	pclk_guard <= pclk_in;
	hsync_guard <= hsync_in;
	vsync_guard <= vsync_in;

	D <= D_guard;
	pclk <= pclk_guard;
	hsync <= hsync_guard;
	vsync <= vsync_guard;
     end

カメラセンサーからのすべての信号は @bus_clk を使ってサンプリング (sampling) されることに注意してください。カメラの PCLK でさえ、他の信号と同じ方法でサンプリングされます。言い換えれば、PCLK はクロックとしてではなくデータ信号として扱われます。このテクニックについては、以下で簡単に説明します。

また、@bus_clk とカメラセンサーの信号との間のタイミング関係は不明です。したがって、これらの信号を受けるフリップフロップの出力は信頼できません。これらのフリップフロップのタイミング要件を保証することは不可能なので、短い時間だけ不安定になる可能性があります。これはクロックドメインをまたぐ際に起きる問題 (clock domain crossing) としてよく知られています。

この問題の解決策は、別のページで説明されているメタステイビリティ (metastability) ガードです。これは、直列に接続された 2 つのフリップフロップを意味します。1 つ目のフリップフロップ(たとえば @pclk_guard)は外部信号に接続されています。2 つ目のフリップフロップは 1 つ目に接続されています。1 つ目が短時間不安定になっても、2 つ目のフリップフロップのタイミング要件は保証されます。したがって、2 つ目のフリップフロップの出力は信頼できます。

まとめると、@D、@pclk、@hsync、@vsync は信頼できるレジスタです(@bus_clk に同期しています)。

@bus_clk の周波数は 100 MHz でしたね。一方、PCLK の周波数は 25 MHz です。したがって、@D、@hsync、@vsync の値は 4 クロックサイクルごとに 1 回だけ使用する必要があります。では、4 つのうちどのクロックサイクルで使うのでしょうか。

その答えは、Verilog コードの次の行にあります。

   wire       sample_valid;
   reg 	      previous_pclk;

   always @(posedge bus_clk)
     previous_pclk <= pclk;

   assign sample_valid = pclk && !previous_pclk;

このコードスニペットの意味は単純です。@pclk が現在 High で、前のクロックサイクルで Low だった場合、@D、@hsync、@vsync の値を使用します。カメラセンサーの信号は、カメラの PCLK が High から Low に変化するときに値が変わります。つまり、PCLK が Low から High に変化するとき、他の信号は安定しています。

しかし、@pclk、@D、@hsync、@vsync はレジスタです。これらのレジスタは @bus_clk に同期しており、特定の時点でのカメラセンサーの信号のスナップショットを表します。ロジックは PCLK 自体の立ち上がりエッジを検出する代わりに、@pclk で似たようなことを行います。@pclk の値が Low から High に変化するとき、それが他のレジスタの値を使用する正しいタイミングです。

このテクニックは 01 信号サンプリング (01-signal sampling) と呼ばれます。このテクニックの背後にある考え方は、01 信号サンプリングに関する別のページで詳しく説明しています。そのページでは、この方法がどのように FPGA のタイミング要件を保証するのかについても説明しています。これにより、ロジックは @D、@hsync、@vsync の値が正しいことを保証します。

データフローの開始と停止

ここでは、ホストへのデータ送信を防ぐための 2 つのレジスタを見てみましょう。

この 2 つのレジスタについて詳しく説明します。まず @wait_for_frame です。

   reg 	      wait_for_frame;

   always @(posedge bus_clk)
     if (!user_r_read_32_open)
       wait_for_frame <= 1;
     else if (sample_valid && vsync)
       wait_for_frame <= 0;

@wait_for_frame の値は、デバイスファイルが開かれていないときに High になります。このレジスタの値は、カメラセンサーの vsync 信号に応答して Low に変わります。この信号は、フレーム間の時間帯に High になります。つまり、@vsync が High の間は、カメラからピクセルデータは送信されません。

まとめると、@wait_for_frame は、カメラからのピクセルデータを無視すべきときに High になります。デバイスファイルが閉じられている場合、またはデバイスファイルを開いたばかりでカメラがまだフレームの途中にある場合です。

次に @fifo_has_been_full について説明します。ホストに届くデータがカメラセンサーの生成するデータと同一であることを保証することが重要です。しかし、コンピュータプログラムがデバイスファイルから十分な速さでデータを読み出さないと、FIFO 内でオーバーフロー (overflow) が発生する可能性があります。DMA バッファが最終的に満杯になり、FIFO の内容をコピーする場所がなくなるからです。その結果、Xillybus IP コアは FIFO からデータを読み出せなくなります。そうなると FIFO は full (満杯) になり、新しいデータを書き込むことができなくなります。

この状況を防ぐためにロジックができることはありません。しかし、ロジックはホストに届くデータの連続性を保証できます。FIFO が full (満杯) になった場合、ロジックは FIFO へのデータ書き込みを停止します。さらに、FIFO が full (満杯) になった後で empty (空) になった場合、ロジックはホストに EOF を送るよう要求します。その結果、コンピュータプログラムは FIFO が full (満杯) になるまでに書き込まれたすべてのデータを受け取ります。その後、コンピュータは EOF を受け取ります。これは通常のファイルの終端に達したときに起こることと同じです。

この仕組みにより、コンピュータプログラムは到着するデータが正しく連続していると信頼できます。連続性が失われると、EOF によってコンピュータプログラムはデバイスファイルを閉じることになります。プログラムがデバイスファイルを再度開くと、@wait_for_frame のおかげで、データは新しいフレームから始まります。

関連する Verilog コードの部分は次のとおりです。

   reg 	      fifo_has_been_nonfull, fifo_has_been_full;
   wire       fifo_full;

   always @(posedge bus_clk)
     begin
	if (!fifo_full)
	  fifo_has_been_nonfull <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_nonfull <= 0;

	if (fifo_full && fifo_has_been_nonfull)
	  fifo_has_been_full <= 1;
	else if (!user_r_read_32_open)
	  fifo_has_been_full <= 0;
     end

   assign user_r_read_32_eof = fifo_has_been_full && user_r_read_32_empty;

@fifo_has_been_full は、FIFO が full (満杯) になったことがあるときに High になります。このレジスタは、デバイスファイルが開かれていないときに Low に変わります。@fifo_has_been_full は、@fifo_full と @fifo_has_been_nonfull が両方 High のときに High になります。

@fifo_full は FIFO の「full (満杯)」ポートに接続されています。では、なぜ @fifo_has_been_nonfull が必要なのでしょうか。その理由は、FIFO がリセット状態にある間は、多くの FIFO が「full (満杯)」を High に保つからです。この機能の目的は、FIFO がまだデータを受け取る準備ができていないことをアプリケーションロジックに伝えることです。@fifo_has_been_nonfull の目的は、このシナリオで @fifo_has_been_full が誤って High になるのを防ぐことです。

@user_r_read_32_eof は、@fifo_has_been_full と @user_r_read_32_empty が両方 High のときに High になります。言い換えると、FIFO が過去に full (満杯) になり、現在は empty (空) の場合に、EOF がホストに送られます。この状況では、いずれにせよ FIFO に新しいデータが書き込まれることはないので注意してください。

データの連続性を保証するための同様の解決策について論じた別のページがあります。そのページで紹介している解決策は、FIFO の両側が異なるクロックドメイン (clock domain) に属している場合に必要です。このページのコードでは FIFO が 1 つのクロックにのみ同期しているため、@fifo_has_been_full の実装はより簡単です。

FIFO へのデータ書き込み

Verilog コードの次の部分は、ピクセルデータを FIFO に書き込みます。

   reg 	      fifo_wr_en;
   reg [1:0]  byte_position;
   reg [31:0] dataword;

   always @(posedge bus_clk)
     if (wait_for_frame)
       begin
	  byte_position <= 0;
	  fifo_wr_en <= 0;
       end
     else if (sample_valid && hsync)
       begin
	  case (byte_position)
	    0: dataword[7:0] <= D;
	    1: dataword[15:8] <= D;
	    2: dataword[23:16] <= D;
	    3: dataword[31:24] <= D;
	  endcase

	  if (byte_position == 3)
	    fifo_wr_en <= !fifo_has_been_full;
	  else
	    fifo_wr_en <= 0;

	  byte_position <= byte_position + 1;
       end
     else
       fifo_wr_en <= 0;

カメラセンサーからのピクセルデータは、8 ビット幅のデータ要素として届きます。このロジックの部分は、FIFO に書き込めるようにこれらのデータ要素を 32 ビットに再編成します。8 ビットの Xillybus ストリーム (/dev/xillybus_write_8) をこの目的で使わない理由は 2 つあります。

@wait_for_frame が High のときは、次の 2 つのいずれかの理由で FIFO に何も書き込まれません。デバイスファイルが開かれていないか、デバイスファイルは開かれているが新しいフレームの先頭にまだ達していないかです。

カメラセンサーの HSYNC が High のとき、データ信号には有効なピクセルが含まれています。「sample_valid && hsync」という式の値は 2 つの基準を組み合わせています。@sample_valid が High のとき、@hsync と @D には有効な値が入っています。したがって、@hsync が High なら、@D の値が @dataword の一部にコピーされます。また、@D が @dataword の最後の部分にコピーされる(つまり @byte_position が 3 の)場合、@fifo_wr_en が High になります。その結果、@dataword が FIFO に書き込まれます。より正確には、@fifo_wr_en の式は次のようになります。

fifo_wr_en <= !fifo_has_been_full;

したがって、前述のとおり、@fifo_has_been_full が High の場合、FIFO には何も書き込まれません。

Verilog コードと実際のピンの関係

上記の Verilog コードは J6 という inout ポートを使っていますが、このポートの接続はどのようにしてピンヘッダに到達するのでしょうか。答えは xillydemo.xdc にあります。このファイルは、ビットストリーム (bitstream) を作成する Vivado プロジェクトの一部です("vivado-essentials" ディレクトリ内にあります)。

xillydemo.xdc には、FPGA が電子部品として正しく動作するために必要なさまざまな情報が含まれています。このファイルには、とりわけ次の行が含まれています。

[ ... ]

## J6 on board (BANK33 VADJ)
set_property PACKAGE_PIN U22  [get_ports {J6[0]}];   #J6/1  = IO_B33_LN2
set_property PACKAGE_PIN T22  [get_ports {J6[1]}];   #J6/2  = IO_B33_LP2
set_property PACKAGE_PIN W22  [get_ports {J6[2]}];   #J6/3  = IO_B33_LN3
set_property PACKAGE_PIN V22  [get_ports {J6[3]}];   #J6/4  = IO_B33_LP3
set_property PACKAGE_PIN Y21  [get_ports {J6[4]}];   #J6/5  = IO_B33_LN9
set_property PACKAGE_PIN Y20  [get_ports {J6[5]}];   #J6/6  = IO_B33_LP9
set_property PACKAGE_PIN AB22 [get_ports {J6[6]}];   #J6/7  = IO_B33_LN7
set_property PACKAGE_PIN AA22 [get_ports {J6[7]}];   #J6/8  = IO_B33_LP7

[ ... ]

最初の行は、信号 J6[0] を U22 に接続すべきであることを示しています。U22 は FPGA の物理パッケージ上の位置です。Smart Zynq の回路図によると、この FPGA ピンはピンヘッダの最初のピンに接続されています。他のポートの位置も同じ方法で定義されています。

まとめ

このページでは、OV7670 からピクセルデータを取得し、Xillybus IP コアを使ってこのデータをホストに送る方法を説明しました。

このチュートリアルの次のパートでは、Xillybus IP コアを使って SCCB(つまり I2C)によりカメラセンサーのレジスタを変更する方法を説明します。これはカメラのパラメータを変更するのに役立ちます。特に、正しい色の画像を得るために必要です。

このページは英語から機械翻訳されたものです。不明な点があれば、原文を参照してください。
Copyright © 2021-2026. All rights reserved. (dcc38493)