この Web ページは、Smart Zynq ボードの機能を探求する小規模プロジェクトのグループに属しています。
このプロジェクトは、HelloFPGA にも掲載されています。中国語の読者にはそちらをおすすめします。
はじめに
このチュートリアルでは、Smart Zynq ボードを使って OV7670 カメラセンサーのレジスタにアクセスする方法を説明します。これは、カメラセンサーからビデオデータを受信する方法を説明した前のページの続編です。
OV7670 には、カメラセンサーのパラメータを設定するための SCCB (Serial Camera Control Bus) インターフェースがあります。SCCB プロトコルは、OmniVision の文書『OmniVision Serial Camera Control Bus (SCCB) Functional Specification』で定義されています。このプロトコルは、よく知られた I2C プロトコルと互換性があります。
カメラセンサーのレジスタにアクセスする主な動機は、カメラが正しい色の画像を生成するようにすることです。しかし、カメラをレジスタ経由で制御することには、他にも利点があります。デジタル信号の電気電流の制御、カメラの明るさと色の自動調整の制御(場合によっては停止)、テストパターンの要求などです。
Zynq のプロセッサには、それぞれが I2C バスマスタを実装した 2 つの内蔵ユニットがあります。これらのユニットの 1 つを使ってカメラセンサーと通信することが可能です。残念ながら、これらの内蔵 I2C ユニットを OV7670 で使ってみたところ、うまく動作しないことがわかりました。おそらく、配線上のノイズが多いことが原因です。専用のプルアップ抵抗 (pull-up resistor) がないことも別の原因として考えられます(この目的には FPGA の内部プルアップが使われていました)。
内蔵 I2C ユニットを使用できないため、代わりに Verilog モジュールがプロジェクトに追加されます。このロジックは、ノイズの多い信号に対してよりうまく対処できるように設計されています。
Vivado プロジェクトへの変更
以下の手順は、前のページに従って作成済みの Vivado プロジェクトに基づいています。
I2C バスマスタの Verilog 実装をこのリンクからダウンロードします。このファイルを verilog/src/ ディレクトリにコピーしてください。次に、このファイルを Vivado プロジェクトに追加します。[File] > [Add Sources…] をクリックし、"Add or create design sources" を選択します。次に [Next] をクリックします。[Add Files] ボタンをクリックし、verilog/src/ ディレクトリにある "i2c_if.v" というファイルを選択します。次に [Finish] ボタンをクリックします。
プロセッサは、2 つの Xillybus ストリームを使ってこのモジュールを制御します。verilog/src/xillydemo.v をテキストエディタで開きます。コードの中で「PART 3」というラベルの付いた部分を削除し、その部分の代わりに次のコードスニペットを挿入します。
/*
* PART 3
* ======
*
* The instantiation of i2c_if demonstrates how to use two Xillybus
* streams to implement an I2C interface with the camera sensor module.
*
*/
i2c_if i2c_if_ins
(
.bus_clk(bus_clk),
.quiesce(quiesce),
.i2c_clk(J6[15]),
.i2c_data(J6[14]),
.user_w_write_8_open(user_w_write_8_open),
.user_w_write_8_wren(user_w_write_8_wren),
.user_w_write_8_data(user_w_write_8_data),
.user_w_write_8_full(user_w_write_8_full),
.user_r_read_8_open(user_r_read_8_open),
.user_r_read_8_rden(user_r_read_8_rden),
.user_r_read_8_data(user_r_read_8_data),
.user_r_read_8_empty(user_r_read_8_empty),
.user_r_read_8_eof(user_r_read_8_eof)
);
または、この変更を行った後の xillydemo.v をここからダウンロードしても構いません。
この変更を行ったら、いつもどおりビットストリーム (bitstream) ファイルを作成します。
i2c_if モジュールは J6[15] と J6[14] に接続されます。これらのポートはピンヘッダ経由でカメラモジュールの SCL と SDA に接続されます。
カメラセンサーのレジスタの変更
カメラに I2C コマンドを送信するコンピュータプログラムは、このリンクからダウンロードできます。このプログラムを Xillinux のファイルシステムにコピーします(たとえば、scp を使うか、TF カードに直接コピーします)。
ファイルのあるディレクトリに移動します。シェルプロンプトで次のコマンドを実行してコンパイルします。
# gcc -Wall -O3 -o i2c i2c.c
このコマンドは何も出力せずに完了するはずです。
プログラムの実行方法は次のとおりです。通常、プログラムが生成する出力も示します。
# ./i2c
Camera sensor's product ID is 0x7673
Reg 0x3d = 0x88 (to be altered)
Reg 0xb0 = 0x00 (to be altered)
Reg 0x6f = 0x9a (to be altered)
Wrote 0x3d = 0x81
Wrote 0xb0 = 0x84
Wrote 0x6f = 0x9f
まず、プログラムはカメラセンサーの製品 ID を含むレジスタを読み取ります。OV7670 カメラセンサーには様々なバージョンがあります。このチュートリアルは、製品 ID が 0x7673 と表示されるカメラセンサーに基づいています。異なる製品 ID を持つ OV7670 カメラセンサーに遭遇することはおそらくないでしょう。製品 ID が異なる場合、カメラモジュールに別のモデルのカメラセンサーが搭載されている可能性があります。
プログラムは、カメラセンサーの画像の色が正しくなるように、必要最小限の変更を行います。この目的のために 3 つのレジスタが変更されます。
変更を行う前に、プログラムはレジスタの現在の値を読み取ります。これらがプログラム出力の次の 3 行です。次に、プログラムは正しい値をこれらのレジスタに書き込みます。
これら 3 つのレジスタの意味
残念ながら、カメラセンサーのレジスタの意味は、部分的にしか文書化されていません。OV7670 のレジスタの多くは「reserved(予約済み)」と定義されています。そのため、一部のレジスタ変更がなぜ必要なのかについての説明はありません。OV7670 のレジスタに関する情報源は数多くあります。レジスタについての手がかりを探すのに最適な場所は、カメラセンサーの Linux ドライバ、ov7670.c です。特に、ドライバ内で定義されている変数 ov7670_default_regs[] には、貴重な手がかりが数多く含まれています。
これらが、i2c.c が 3 つのレジスタを変更する理由について得られている説明です。残念ながら、これらの変更のいくつかの理由はわかっていません。変更が必要であることは明らかですが。
- COM13 (0x3d): まず、このレジスタのビット 0 を '1' に変更します。これにより、カメラセンサーの出力では U と V の位置が入れ替わります。これは、出力形式を UYVY にするために必要です。UYVY は、mplayer などのソフトウェアが期待する形式です。さらに、このレジスタのビット 3 は '0' に変更されます。このビットの意味はカメラセンサーの文書には書かれていません。
- Reserved レジスタ (0xb0): このレジスタについては文書がありません。
- AWBCTR0 (0x6f): このレジスタは、カメラセンサーのホワイトバランスに関係しています。OV7670 の実装ガイドによると、このレジスタに 0x9f を書き込むと、2 つの変更が行われます。Advanced AWB モードが有効になり、最大カラーゲインが 2 倍から 4 倍に変わります。このレジスタを変更しないと、ホワイトバランスがうまく機能しません。
他のレジスタへの書き込み
@writelist の定義は次のとおりです。これは i2c.c プログラムの冒頭付近にあります。
static const struct {
int addr;
int value;
} writelist [] = {
{ 0x3d, 0x81 }, // COM13, swap UV, turn off reserved bit 3
{ 0xb0, 0x84 },
{ 0x6f, 0x9f }, // AWBCTR0, crucial for white balance
{ -1, -1 }, // Terminate
};
@writelist 配列の各要素は 2 つの数値から構成されます。1 つ目の数値はレジスタのアドレスです。2 つ目の数値は、このレジスタに書き込む値です。
たとえば、最初の要素は { 0x3d, 0x81 } です。これは、COM13 に値 0x81 が書き込まれることを意味します。このレジスタのアドレスは 0x3d です。
配列の最後の要素は { -1, -1 } でなければなりません。
カメラセンサーのドライブ電流の低減
カメラセンサーと Smart Zynq ボードの間のワイヤが長すぎると、ビデオ画像が不安定になる可能性があります。ビデオフレームが飛び、画像全体に緑と紫の縞模様が現れます。これは、ワイヤ間に発生するクロストークが原因でよく起こります。
この問題は、カメラセンサーがワイヤに流す電気電流を減らすことで解決できる場合があります。これを行うには、COM2 に値 0x00 を書き込みます。このレジスタのアドレスは 0x09 です。
つまり、@writelist の定義を次のように変更します。
static const struct {
int addr;
int value;
} writelist [] = {
{ 0x09, 0x00 }, // Drive current to 1x level
{ -1, -1 }, // Terminate
};
その後、プログラムをコンパイルして、以前と同じように実行します。
その他の可能性
カメラセンサーの文書(特に『OV7670/OV7171 CMOS VGA (640x480) CameraChip Implementation Guide』)には、他のいくつかのレジスタに関する情報も記載されています。前述のように、Linux カーネルのドライバも重要な手がかりを提供します。
このチュートリアルの第 1 部で、カメラセンサーを次のコマンドでリセットできることを思い出してください。
# echo 1 > /dev/xillybus_write_32
このコマンドにより、すべてのレジスタがデフォルト値に戻ります。
すべてのレジスタの値の出力
これは、i2c.c プログラムの main() 関数の一部です。
if (0) { // Change this in order to print out registers instead
for (i=0; i<=0xc9; i++) {
i2c_read(i, &value);
printf("Reg 0x%02x = 0x%02x\n", i, value);
}
return 0;
}
この部分の目的は、すべてのレジスタの値を表示することです。通常、この部分は "if (0)" という条件があるため実行されません。すべてのレジスタ値の出力を得るには、これを "if (1)" に変更します。
すべてのレジスタの出力は 1 秒もかかりません。プログラムの実行が一瞬止まる、またはプログラムが固まる場合は、I2C バス上の通信エラーが原因です。その場合、プログラムの出力が正しくない、または不完全になることがあります。プログラムが速くスムーズに実行されるまで、再実行してください。
すべてのレジスタの出力結果はこのリンクからダウンロードできます。この出力結果は、カメラセンサーが正しい色の画像を生成しているときのレジスタ値を反映しています。デフォルト値(カメラセンサーをリセットした直後)の出力結果はこちらからダウンロードできます。カメラは、自動輝度制御やホワイトバランスなどの結果として、一部のレジスタを継続的に変更することに注意してください。
I2C 書き込み操作の実行方法
このセクションを理解するには、I2C プロトコルの基礎知識が必要です。
i2c.c プログラムは、/dev/xillybus_write_8 と /dev/xillybus_read_8 という 2 つの Xillybus ストリームを介して FPGA 内の i2c_if.v モジュールと通信します。
I2C 書き込み操作は次のように行われます。
- ホストが /dev/xillybus_write_8 を開くと、FPGA は I2C のスタートコンディション (start condition) を生成します。
- このデバイスファイルに書き込まれたバイトは、(変更なしで)I2C の配線上に現れます。
- ホストが /dev/xillybus_write_8 を閉じると、FPGA は I2C のストップコンディション (stop condition) を生成します。
これらの手順は、i2c_write() 関数によって実装されています。
static void i2c_write(int addr, unsigned char data) {
unsigned char sendbuf[3] = { i2c_addr << 1, addr, data };
allwrite(sendbuf, sizeof(sendbuf));
}
この関数は、3 バイトからなるバッファを準備します。
- I2C アドレス。書き込み操作の場合、これは 0x42 です。
- レジスタのアドレス。
- レジスタに書き込む値。
FPGA はこれらの 3 バイトを I2C バス経由でカメラセンサーに送信します。i2c_write() 関数は /dev/xillybus_write_8 を開き、バッファのデータを書き込み、ファイルを閉じます。
I2C プロトコルでは、バス上で送信される各バイトに対して、受信側が応答 (ACK) を返さなければなりません。各バイト(8 ビット)には、この目的のための 9 ビット目があります。この 9 ビット目は、送信中に特別なタイムスロットを持ちます。バイトを受信した側は、このタイムスロットの間に SDA 配線を '0' に引っ張ることで、バイトを受信したことを確認します。
カメラセンサーがこのように応答しない場合、FPGA 内の i2c_if モジュールは、デバイスファイルを介したそれ以上のバイトの受け入れを拒否します。これはエラーを引き起こしませんが、close() の関数呼び出しは 1000 ms の遅延の後にのみ戻ります。その理由は、Xillybus ドライバがファイルを閉じる前に、残りのすべてのデータが FPGA に到達するのを待つためです。しかし、I2C スレーブがバイトに ACK を返していない場合、FPGA は次のバイトの受け入れを拒否します。この状況では、ドライバは 1000 ms 待ってから、とにかくファイルを閉じ、次のメッセージをカーネルログに追加します。
Timed out while flushing. Output data may be lost.
カーネルログのメッセージは、"dmesg" コマンドで表示できます。
まとめると、i2c_write() の関数呼び出しが完了するまでに 1 秒かかる場合、カメラセンサーが I2C バスの操作に正しく応答していない可能性が高いです。カメラセンサーが FPGA に誤って接続されているか、まったく接続されていない可能性があります。
I2C 読み取り操作の実行方法
読み取り操作は 2 つの別々の操作からなるため、より複雑です。
- 書き込み操作ですが、データバイトは含まれません。この操作の目的は、レジスタのアドレスを I2C スレーブに送信することです。
- 読み取り操作。レジスタの値がスレーブからマスタに送信されます。
i2c_read() 関数を以下に示します。
static void i2c_read(int addr, unsigned char *data) {
int fdr;
unsigned char cmdbuf[2] = { i2c_addr << 1, addr };
unsigned char dummybuf[2] = { (i2c_addr << 1) | 1, 0 };
allwrite(cmdbuf, sizeof(cmdbuf));
// We open xillybus_read_8 only now. Had it been open during the first
// operation, there would have been a restart condition rather than a
// stop condition after the first command.
fdr = open("/dev/xillybus_read_8", O_RDONLY);
if (fdr < 0) {
perror("Failed to open /dev/xillybus_read_8 read-only");
exit(1);
}
allwrite(dummybuf, sizeof(dummybuf));
allread(fdr, data, sizeof(*data));
close(fdr);
}
この関数は、まず 2 バイト (@cmdbuf) をスレーブに送信します。
- I2C アドレス。書き込み操作と同じ 0x42 です。
- 読み取りたいレジスタのアドレス。
次に i2c_read() は /dev/xillybus_read_8 を開きます。allwrite() がそうであるのとは異なり、これは allread() によって行われるわけではないことに注意してください。
次に i2c_read() は、allwrite() によって 2 バイト (@dummybuf) をバスに書き込みます。
- I2C アドレス。これは 0x43 で、スレーブに対してバス上の操作が読み取りであることを伝えます。
- 0 を含むバイト。i2c_if モジュールはこのバイトの内容を無視します。
FPGA 内の i2c_if モジュールは、受信した最初のバイトのビット 0 を調べます。これに基づいて、FPGA はバス上で書き込み操作と読み取り操作のどちらを行うべきかを判断します。読み取り操作が必要な場合、他のすべてのバイトの内容は無視されます。これらのバイトは、FPGA に何バイト受信すべきかを知らせるためだけのものです。
i2c_if モジュールは、バスから要求されたバイト数を読み取り、/dev/xillybus_read_8 を介してホストに送信します。このデバイスファイルは、@dummybuf の書き込みによってバス上の読み取り操作が開始される前に開かれていなければなりません。この後、allread() がレジスタの値を読み取ります。allread() はファイルを開閉しません。ファイルは早い段階で開いておく必要があるからです。
バスのリスタート
このセクションの内容は OV7670 カメラセンサーには関係ありません。ただし、i2c_if を別のスレーブと使用する場合には役立つかもしれません。
i2c_read() は allwrite() を 2 回呼び出していることに注意してください。呼び出しのたびに、/dev/xillybus_write_8 が開閉されます。その結果、データ送信の前に I2C のスタートコンディション (start condition) が生成され、後にストップコンディション (stop condition) が生成されます。
言い換えると、レジスタのアドレスがスレーブに送信された後にはストップコンディションがあり、マスタが読み取り操作を開始する前にはスタートコンディションがあります。
カメラセンサーはこの一連の流れを期待します。しかし、このように読み取り操作を試みると、他の I2C スレーブとして機能する電子部品は正しく動作しない場合があります。これらの部品は、ストップコンディションによってレジスタのアドレスを忘れてしまいます。そのため、バス上の最初と 2 番目の操作の間にリスタートコンディション (restart condition) を生成する必要があります。
i2c_if モジュールはこの可能性をサポートしています。/dev/xillybus_read_8 を開き続けたまま /dev/xillybus_write_8 が閉じられて再度開かれると、代わりにバス上でリスタートコンディションが生成されます。つまり、スレーブがリスタートコンディションを必要とする場合、allwrite() の関数呼び出しを移動する必要があります。すると i2c_read() は次のようになります。
fdr = open("/dev/xillybus_read_8", O_RDONLY);
if (fdr < 0) {
[ ... ]
}
allwrite(cmdbuf, sizeof(cmdbuf));
allwrite(dummybuf, sizeof(dummybuf));
allread(fdr, data, sizeof(*data));
close(fdr);
繰り返しますが、このコードは OV7670 には適していません。
まとめ
Xillybus IP コア (IP core) を使って、カメラセンサーのレジスタにアクセスすることができます。I2C バスとのインターフェースには、追加のモジュール i2c_if が必要です。このモジュールは、他の I2C スレーブとの通信にも役立ちます。
OV7670 カメラモジュールのレジスタについて入手できる情報は、残念ながら不足しています。そのため、インターネットで解決策を探したり、カメラセンサーの Linux ドライバから支援を得たりする必要があるかもしれません。