Quantcast
Viewing all 48 articles
Browse latest View live

ZYBOのPSでI2Cを動かしてみた

ZYBOのPS(ARM Core部分)のI2Cを動かしてみました。本当は、OV7670カメラモジュールを使って画像の取り込みをやってみたかったのですが、Amazonで買ったOV7670モジュールがどうも不良品のようで、SCCB(I2Cのサブセットのカメラモジュール制御プロトコル)を使ってカメラモジュールとどうしても通信ができず、その過程で分かったIC2の使い方を書いています。

カメラモージュールの実験は、代替え品をaitendoさんに注文したので、商品が届いたら出直しです。Amazonのもaitendoさんのもカメラモジュール自体は同じものを使っていると思いますが、回路構成が若干異なり、aitendoの製品の方が使いやすと思います。理由は、Amazonで販売しているモジュールはパワーオンリセットやI2C信号線のプルアップがないためです。ペリフェラル側にプルアップがあると、ブレッドボードなどを経由してプルアップの配線をする必要がないので構成がスッキリします。

プルアップはFPGA内蔵のプルアップ機能を使う手もありますが、外付け抵抗を使ったプルアップの方が安定して動くと思います。ちなみに、今回の実験で使ったTMP102温度センサーは外付けプルアップ抵抗が必要で、FPGAのプルアップではクロックを10KHzに落としても正常に動作しませんでした。FPGAの内蔵プルアップはweak pull-upなので高速動作には使えないというようなフォーラムの書き込みがありました。

Update:FPGA内蔵のプルアップで動作しなかったのは、200Ωの保護抵抗が直列に入っているStandard Pmod (JE)にI2Cセンサーをつないだ時の場合でした。試しに保護抵抗が入っていない、Hi-Speed Pmod (JD)にFPGA内蔵のプルアップでつないでみたたらクロック100KHzでも動作しました。条件がよければFPGA内蔵のプルアップでも動作しますが、10KΩ程度の外部抵抗を使った方がより安定していると思われます。

ZYNQ PSのI2Cを使う方法

ZYBOでZYNQ PSのI2Cを使う方法は、以下の2通りのやり方があります:

  • PL(FPGAブロック)を介して、ZYBOのPmodコネクタ(JB〜JE)に接続する
  • PS(CPUブロック)直結のPmod MIO(JF)に接続する

ここでは、それぞれの場合について試してみます。開発環境は執筆時点で最新の、Vivado 2016.2を使用しています。

PL(FPGAブロック)を介して、ZYBOのPmodコネクタ(JB〜JE)に接続する方法

Vivadoで新規プロジェクトを作成して、IP Integrator(IPI)で新規のデザインを作成します(今回はsystemというデザイン名にしています)。図のようにZYNQ 7 Processing Sysemのみをインスタンス化してクロックの接続を手動で行います。

Image may be NSFW.
Clik here to view.
Block Design

ZYNQ 7 Processing Systemのアイコンを右クリックして、”Customize Block..”メニューを開きます。

Image may be NSFW.
Clik here to view.
Customize Block

MIO Configurationをクリックして、IO Peripheralsのプルダウンを開き、I2C0にチェックを入れます。また、IOに「EMIO」を指定します。EMIOを指定することによって、CPUコアのI2C信号がFPGAのPL部分を通って外部に接続できるようになります。

Image may be NSFW.
Clik here to view.
I2C EMIO

ブロックデザインに戻って、ZYNQ 7のIIC_0を右クリックして”Make External”メニューを選択します。この操作によって、I2Cの信号をFPGAから外部に出力できるようになります。外部出力を作る方法には、”Create Interface Port.."などオプション指定ができる方法もありますが、今回はMake Externalで問題ありませんでした。

Image may be NSFW.
Clik here to view.
I2C Make External

次に、Block DesignのSource画面に移って、デザイン名(今回の場合はsystem)を右クリックし、”Create HDL Wrapper..”を選択します。

Image may be NSFW.
Clik here to view.
CreateHDL Wrapper

以下のダイアログボックスが表示されるので、”Let Vivado manage wrapper and auto-update”を選択してOKをクリック。

Image may be NSFW.
Clik here to view.
Let Vivado Manage Wrapper

Flow Navigatorから「Run Implementation」を実行。Implementationが終了すると以下のダイアログボックスが表示されるので、”Open Implemented Design”を選択。

Image may be NSFW.
Clik here to view.
Implementation Complete

画面下に表示される「IO Ports」タブを開くとIIC_0ポートのピンアサイン画面が表示されます。ここに信号を接続したいFPGAのピン番号を入力します。

Image may be NSFW.
Clik here to view.
IO Port

ZYBOのReference Manualを参照して、今回は信号をPmod JE(Standard Pmod)のJE1とJE2に接続します。それぞれに対応するFPGAのピン番号V12とW16を入力、出力電圧をLVCMOS33(3.3V)に指定します。

Image may be NSFW.
Clik here to view.
Assign PL Pin

CTL-Sキーを押すと、Constraintsを保存するダイアログボックスが表示されるのでOKをクリック。

Image may be NSFW.
Clik here to view.
Save Constraints

ファイル名を指定して保存すると、SourcesにConstraints(制約)ファイルが追加されています。

Image may be NSFW.
Clik here to view.
Constraints Added

続けて、Flow Navigatorから「Generate Bitstream」を実行。実行が完了すると以下のダイアログボックスが表示されるので、”Open Implementation Design”を指定してOKをクリック。

Image may be NSFW.
Clik here to view.
Genarate BitStream

File Menu → Export → Export Hardware..を選択。Include bitstreamをチェックしてOKをクリック。

Image may be NSFW.
Clik here to view.
Expot Hardware

File Menu → Launch SDKを選択。SDKが立ち上がります。

Image may be NSFW.
Clik here to view.
SDK Launched

SDKのFile Menu → New → Application Projectを選択。New Projectの設定画面にプロジェクト名(今回はI2C Test)を入力し、Nextをクリック。

Image may be NSFW.
Clik here to view.
New Project

Hello World Templateを選択します(このテンプレートにはprint文を使ってUARTにデバッグ情報を出力するために必要なファイルが含まれているため)。

Image may be NSFW.
Clik here to view.
Select Hello World

プロジェクトが生成されたら、helloworld.cをリネーム。今回は、i2c_test.cにしています(これは好みですが)。

Image may be NSFW.
Clik here to view.
Rename helloworld c

テンプレートが自動生成したソースを全部削除して、以下のコードを入力。 

 
#include "platform.h"
#include "xparameters.h"
#include "sleep.h"
#include "xiicps.h"
#include "stdio.h"


// I2C parameters
#define IIC_SCLK_RATE		100000	// clock 100KHz
#define TMP102_ADDRESS		0x48	// 7bit address
#define IIC_DEVICE_ID		XPAR_XIICPS_0_DEVICE_ID

XIicPs Iic;

int Init()
{
	int Status;
	XIicPs_Config *Config;	/**< configuration information for the device */

	Config = XIicPs_LookupConfig(IIC_DEVICE_ID);
	if(Config == NULL){
		printf("Error: XIicPs_LookupConfig()\n");
		return XST_FAILURE;
	}

	Status = XIicPs_CfgInitialize(&Iic, Config, Config->BaseAddress);
	if(Status != XST_SUCCESS){
		printf("Error: XIicPs_CfgInitialize()\n");
		return XST_FAILURE;
	}

	Status = XIicPs_SelfTest(&Iic);
	if(Status != XST_SUCCESS){
		printf("Error: XIicPs_SelfTest()\n");
		return XST_FAILURE;
	}

	XIicPs_SetSClk(&Iic, IIC_SCLK_RATE);
	printf("I2C configuration done.\n");

	return XST_SUCCESS;
}

int i2c_write(XIicPs *Iic, u8 command, u16 i2c_adder)
{
	int Status;
	u8 buffer[4];
	buffer[0] = command;

	Status = XIicPs_MasterSendPolled(Iic, buffer, 1, i2c_adder);

	if(Status != XST_SUCCESS){
		return XST_FAILURE;
	}

	// Wait until bus is idle to start another transfer.
	while(XIicPs_BusIsBusy(Iic)){
		/* NOP */
	}

	return XST_SUCCESS;
}


int i2c_read(XIicPs *Iic, u8* buff, u32 len, u16 i2c_adder)
{
	int Status;

	Status = XIicPs_MasterRecvPolled(Iic, buff, len, i2c_adder);

	if (Status == XST_SUCCESS)
		return XST_SUCCESS;
	else
		return -1;
}


int main()
{
	init_platform();
	Init();

	u8    buff[4];
	u16   rawdata;
	float temp;

	while(1) {
		i2c_write(&Iic, 0, TMP102_ADDRESS);
		i2c_read(&Iic, buff, 2, TMP102_ADDRESS);

		rawdata = ((int8_t)buff[0] << 4) | ((u8)buff[1] >> 4);
		temp = (float) ((float)rawdata * 0.0625);
		printf("Tmep: %2.1f\n", temp);
		usleep(1000*1000);		// sleep 1sec (1000 x 1000us)
	}

	cleanup_platform();
	return 0;
}

ファイルをセーブすると自動的にビルドが実行されます。次に、ツールバーのProgram FPGAボタンをクリックしてFPGAのコンフィグデーター(Bitstream)をJTAGインタフェース経由で転送します(ZYBOのJP5ジャンパーピンをJTAGに設定しておくこと)。

Image may be NSFW.
Clik here to view.
Program FPGA

続いて、デバッガーを起動してPS(ARM CPUコア)のプログラムを転送しますが、ちょっとコツがあります。私の環境では、Debgug Configurationをいきなり作って起動しようとするとエラーが出てプログラムの起動に失敗することが多いです。そのため、Project ExplorerのI2C_Testプロジェクトを右クリックし、Debug As → Launc on Hardware (GDB)を選択してまずプログラムを起動します。無事プログラムが起動するとmainの最初の行でプログラムがブレークします。

Image may be NSFW.
Clik here to view.
Debug Started

この段階ではDebug Configurationをしていないため、"STDIO not connected”の警告がConsoleに出力され、print文の実行結果は表示されません。ここで一旦、ツルーバーのTerminateボタンを押してプログラムを終了します。デバッグPerspectiveから一旦C/C++ Perspectiveに戻ってProject Explorer → I2C_Testプロジェクトを右クリック → Debug As → Debug Configurations..を選択。STDIO ConnectionにCOMポート番号と通信速度(115200)を設定してDebugをクリック。ツールバーのResumeボタンをクリックするとプログラムが動き出します。

Image may be NSFW.
Clik here to view.
Debug Configurations

TMP102から読み取った温度がConsole画面に表示されています。

Image may be NSFW.
Clik here to view.
Program Run

今回使っているI2CドラバーライブラリのサンプルやドキュメントはSDKのインストールフォルダーの中(C:\Xilinx\SDK\2016.2\data\embeddedsw\XilinxProcessorIPLib\drivers)にあるので、コードの中身の説明は割愛します。

以上が、I2Cの信号をFPGA(PL)経由で取り出す方法です。FPGA内の配線を見ると、I2CのSDAなどI/Oの信号はinとoutの2本の独立した信号としてCPUから出ており、Tri-state Bufferを介してI/Oの信号として外部ピンに接続されています。なんだが回りくどいことをやってるんですね。

Image may be NSFW.
Clik here to view.
SDA iobuf

このIOBUFはVivadoのIPをWrapperの中でインスタンス化して接続していることが以下のHDLコードから分かります。

Image may be NSFW.
Clik here to view.
Wrapper IOBUF

PS(CPUブロック)直結のPmod MIO(JF)に接続する方法

SDKを終了し、VivadoのBlock Designを開き、IIC_0ポートを削除します。

Image may be NSFW.
Clik here to view.
DeleteI 2C Port

Customize Block..を開き、I2C0の接続先を「MIO 10..11」に変更します。

Image may be NSFW.
Clik here to view.
Assign I2C MIO

OKをクリックし、Run Implementationを実行します。実行後のI/O Portsタブを見ると、I2Cの信号が消えていることが分かります。これはI2Cの信号がFPGAブロック(PL)を経由しなくなったことを意味します。

Image may be NSFW.
Clik here to view.
No I2C In PL

Generate Bitstreamを実行し、終了後、Export Hardwareを実行して再度SDKを立ち上げます。SDKのProject ExplorerからI2C_Test_bspを右クリックして、Re-generate BSP Sourcesを実行。さらに、Project Menu → Clean..を実行してビルド環境をクリーンアップします。

Image may be NSFW.
Clik here to view.
Clean Build

センサーのI2CのピンをJFコネクタのMIO-10 (JF2: SCL)、MIO-11 (JF3: SDA)に繋ぎ変えてデバックを実行するとプログラムが起動します。今回はCPUから直接I2Cに出力しています。デバッグでプログラムの起動に失敗する場合は、Debgug Configurationに入って既存のデバッグエントリを一旦削除してから、最初の手順ででデバッグを際実行する(先ずはDebug Configを作らずにDebug Asから起動する)とうまくいくと思います。

Image may be NSFW.
Clik here to view.
Delete Debug Target

ということで、PL経由・PS直結のどちらでもI2Cが動くことが確認できました。

参考情報


オシロ(RIGOL DS1054Z)を購入しました

最近FPGA(特にZYBO)をいじるようになって、ちょっとした信号波形の観測がしたいケースが増えてきました。そこで、とうとうオシロスコープを買ってしまいました。以前は日曜大工の電子工作にオシロスコープはもったいないと思って購入を躊躇していたのですが、いざ買って使ってみるとやはり便利です。

購入した機種はRIGOL DS1054Z(中国製)でAmazonから購入しました。同価格帯(5万円台)で買える、Tektronix TBS1052Bとどちらにしようかと少し悩んだのですが、4ch vs 2chでRIGOLの方がch数が多いこと、その他測定機能もRIGOLの方が充実しているように見えたのと、Amazonや他のBlogのレビューを見ても、RIGOLの評価は概ね良かったので、ブランド的にはTextronixですがコスパを取ってRIGOLにしました。

Image may be NSFW.
Clik here to view.
IMG 1175

オシロスコープは学生時代にアナログオシロをちょっと触ったことがある程度で、ディジタルオシロは初めてだったので、まだ使いこなせていませんが、個人用の電子工作用途にはこれで十分だと思います。やっぱり実際の信号波形が見えると動作確認の効率が飛躍的に上がるので、買って良かったと思っています。

マニュアル・表示ともに日本語にも対応しており、マニュアルの日本語が不自然なこともありません(ただ、英語版に比べて更新がやや遅いようですが)。DS1054ZはオプションでI2Cのプロトコル解析やデコードにも対応しているのですが(買った状態では使用時間限定で動かすことができます)、マニュアルを見ただけではさっぱり使いたかが分かりませんでした。Webで検索するとYou-Tubeに実際に操作している画像がアップされており、それを見て使い方が分かりました。英語が主体になりますが、サポート情報も比較的充実しているのでその点も良いと思います。

Image may be NSFW.
Clik here to view.
OV7670 25V PU

今の所気になる点は、ファンの動作音がややうるさいこと(You Tubeには自分でファンを静音型のもの交換している画像もありました)、画面表示をUSBメモリーに保存できるのですが本体にRTCがないらしく、ファイルのタイムスタンプが2015年の固定の日付になってしまい、母艦のMacにコピーした際に時間順にソートできないことです(コンソールから毎回touchコマンドでタイムスタンプを更新する必要があり面倒)。

とは言え、機能的には十分満足しています。まだまだ使いこなせていないので、色々触ってみたいと思っています。

ZYBOでOV7670カメラモジュールの画像を表示する

前回のポストでZYBOのZYNQ PS (ARMコア)を使ってOV7670カメラモジュールをI2C(SCCB)経由初期化できるようになりましたが、ようやく次のステップとしていた画像の取り込みとVGAモニターへの出力ができました。使ったカメラモジュールはaitendoさんから購入したOV7670です。

色々紆余曲折(最初の構想からの見直し)やFPGAを使った同期回路設計のスキル不足でかなり苦戦して、たかだか100行ちょっとのRTLコードを動かすのに1ヶ月近くかかってしまいました。まだまだ未熟ですが、(自己流ですが)少しノウハウがたまったので、顛末を書いてみます。

使用した開発環境は、最初はVivado 2016.2でしたが、最終的にVivado  2016.3にアップグレードしています。

当初の構想

最初は、既存のXILINX Video-OutとVTC IPを使って、画像の取り込み部分だけを作るのが近道かと思い(自分で記述する部分を最小にした方が確実に動かせると思っていた)この方向で情報収集を行いました。

Video Out IPに画像を流し込むためには、カメラ入力側にAXI4-Stream Masterインタフェースを作りこむ必要があります。今にして思うと、いきなりAIX4を作るというのも無謀な試みでした。You TubeにAXI-Stram Masterを作るチュートリアルビデオがあり、(英語ですが平易な語りなのでなんとか理解できますが、量は結構多い)これを参考にカメラデータの取り込みとAXI4-Stram出力のIPを作ってみました。単体のテストベンチではAIX4の制御信号を出力できているように見えていたのですが、Video OutやVTCと結合していきなり実機で動かそうとしても全く画像が表示されず。

当初、OV7670の解像度がVGA(640 x 480)なのでVGAモニターにスルーでデーターを流せると思っていたのですが、オシロで出力波形を観測すると垂直同期(リフレッシュ周期)が24Hz〜30Hz, 水平同期が12.5KHz位で、VGAのリフレッシュレート60Hz, 水平同期31KHzに対して速度が大きく異なるため、そもそもスルーで出力できないということが判明(考えてみれば、3000円以下のカメラモジュールで60Hzのリフレッシュレートを期待するのが間違っている)。Video OutやVTC IPもブラックボックスで中身が分からないため、問題の切り分けができず、表示部分も自分で作った方がよいと方針転換。

AXI4-Stramの実装は結局使い物にはなりませんでしたが、チュートリアルで学んだことはVivadoを使った開発フローを含めて結構有意義でした。

VGA画面表示も自分で作る

カメラモジュールとVGAの動作速度が異なるため、速度差を吸収するために、VRAM(Dual Port RAM)を間に入れて、カメラ入力に同期して画像データをVRAMに書き込み、VGAのPixelレートで画像データーを読み出せるようにする必要があります。

VGAのフル解像度を使うためには、必要なメモリー容量的にZYBOのDDR3-SDRAMを使う必要がありますが、いきなりDDR3-SDRAMを使うのはハードルが高いので、先ずはFPGA内に作れるBRAM(Block RAM)を使うことにしました。ZYBOに搭載されているZYNQ XC7Z010-1CLG400CのBRAM容量の制約から、VGAのフル解像度を使うのは諦めて、QVGA(320 x 240)で画像を取り込んで、VGA画面(640 x 480)の中央に320 x 240の画像を表示できるようにすることを目標に再設定。

このblogにあるVHDLの実装を参考に(というかほとんどパクっていますが…)Verilogに焼き直して、画像取り込みとVGA出力のIPを作成。参考にしたBlogはRGB444(12bitカラー)の画像データーをBRAMに取り込む際に、下位1bitを抜いた形でメモリーに格納していますが(BRAMの容量制約のために、19bit中の上位18bitをアドレスに使っている)、このようなデーターの間引きを行うとどういう形で画像データーが表示できるのかイメージができなかったのと、全部パクリでなく、多少オリジナルにしたかったため、RGB 565(16 bitカラー)のQVGAデーターを扱えるように改造しました。

再度カメラ画像取り込みとVGA表示のIPを作って単体でテストベンチを動かすと一応動いているように見えるので、BRAMやカメラモジュールと結合して実機で動かすとやはり画像が出力されず(画面が真っ暗)。オシロでVGA出力の信号を観測すると、VSYNCとHSYNCは正常に出力されているので、部分的には動作しているがどこが悪のか切り分けができず(後で、BRAMの読み出し側クロックを配線していなかったという、初歩的なチョンボだったことが分かるのですが..)。

モジュール単位のテストベンチだけでなく、全体を一気通貫で確認できるシミュレーション環境が必要と思い、以下のようにCPUブロックを除いた、カメラ画像取り込み→ BRAM → VGA出力のIPをつないだBlock Designを作ってシミュレーション環境にしました。

Image may be NSFW.
Clik here to view.
Test Blcok Design

Test Benchを作る際は、Create HDL Wrapperが生成するBlock Designのインスタンスを雛形にして、試験信号生成の部分を追加記述しています。まず、以下の手順でWrapperファイルを生成。

Image may be NSFW.
Clik here to view.
Create HDL Wrapper

生成されたWrapperファイルをベースに以下のテストベンチを記述

 
//Copyright 1986-2016 Xilinx, Inc. All Rights Reserved.
//--------------------------------------------------------------------------------
//Tool Version: Vivado v.2016.3 (win64) Build 1682563 Mon Oct 10 19:07:27 MDT 2016
//Date        : Tue Oct 18 21:28:14 2016
//Host        : iMac2-Win running 64-bit major release  (build 9200)
//Command     : generate_target ov7670_tb_wrapper.bd
//Design      : ov7670_tb_wrapper
//Purpose     : IP block netlist
//--------------------------------------------------------------------------------
`timescale 1 ns / 1 ps

module ov7670_tb_wrapper();

  reg  clka;
  reg  clk25;
  reg  href;
  reg  vsync;
  reg  pclk;
  reg  resetN;
  wire [4:0]vo_b_data;
  wire [5:0]vo_g_data;
  wire [4:0]vo_r_data;
  wire vo_hsync;
  wire vo_vsync;
  reg  [17:0] count;
  reg  [7:0]  test_data;
  reg  [7:0]  test_vector[307200:0];

  // Instantiate device to be tested
  ov7670_tb ov7670_tb_i(
         // Instantiate ov7670_camera
        .pclk(pclk),
        .href(href),
        .data(test_data),
        // Instantiate VGA
        .clk25(clk25),
        .resetN(resetN),
        .vo_b_data(vo_b_data),
        .vo_g_data(vo_g_data),
        .vo_hsync(vo_hsync),
        .vo_r_data(vo_r_data),
        .vo_vsync(vo_vsync),
        .vsync(vsync)
        );


  // Initialize test
  initial
  begin
    resetN <= 0;
    count  <= 17'd0;
    vsync  <= 0;
    href   <= 0;
    #100; resetN <= 1;
  end

  // Generae vsync and href
  // 1t_LINE = (320 + 144) x pclk(84ns)
  initial
  begin
    $readmemh("testdata.txt", test_vector);
    #400; vsync <= 1;
    #400; vsync <= 0;
    #960;
    repeat (240)
      begin
        @(negedge pclk);
        href <= 1; #26880;  // 320 pclk per line
        href <= 0; #1096;   // 144 pclk
      end
    
    #2000;
    
    $display("Vsync");
    #1000; vsync <= 1;
    #400;  vsync <= 0;
    #960;
    repeat (240)
      begin
        @(negedge pclk);
        href <= 1; #26880;  // 320 pclk per line
        href <= 0; #1096;   // 144 pclk
      end

    #400;
    $finish;
  end


  // Generate clcok  12MHz camera pclk in => 84ns
  always
  begin
    pclk <= 1;
    if (href)
      begin
        test_data <= test_vector[count];
        count <= count + 1;
      end
    #42;
    pclk  <= 0;
    #42;
  end

  always
  begin
    clk25 <= 1; #20; clk25 <= 0; #20;   //  25MHz VGA pixel clock
  end

endmodule

試験用のダミーデータを読み込んで、2画面分の出力を行なっています。シミュレーション波形を見ると、hrefが有効になった後、2バイトのデーターを取り込んで、最初のweでBRAMのアドレス0x00000にデーターが書き込めています。タイミング的には動いている模様。

Image may be NSFW.
Clik here to view.
Simulation

BRAMの設定は以下のように、Stand Alone/ Simple Dual Port RAMにしています。

Image may be NSFW.
Clik here to view.
BRAM Setting-1

BRAMのOperating ModeはWrite Firstを選択。BRAMへの書き込みと読み出しクロックは非同期ですが、書き込みと読み出しが競合する可能性がある場合は、Write Fisrtを推奨するとマニュアルに書いてあったのでそうしています。確かにテストベンチのシミュレーションでもcollisonの警告が出ていたりしますが、書き込みと読み出しの競合は避けられないので仕方ありません。

Image may be NSFW.
Clik here to view.
BRAM Setting-2

実機で動作させる

以下のようにPSコアとClock生成を組み込んだBlock Designを作成して実機で動作確認。OV7670の初期化はARMコアのソフト処理で行なっています。

Image may be NSFW.
Clik here to view.
Final Desing

またもや、画面が真っ黒、VSYNCとHSYNCは出ている、、
しばらく悩みましたが、VGA出力側のクロックをBRAMに配線し忘れていたという、超初歩的なミスが発覚。

配線を修正して動かしてみたら、やっと画像が表示されたが、こんな感じでえらく不鮮明。カメラの設定はちゃんとRGBにしているし(YUVとかになっているわけではない)。しばらく、カメラの設定が間違っていないかデーターシートとにらめっこをしながら確認しても、特におかしな箇所は見つからず。 

Image may be NSFW.
Clik here to view.
Camera Image NG

しばらく悩んだのですが、カメラモジュールを壊したのではないかと結論づけて、2個目を購入。2個目が届いて入れ替えたら、やっと動きました!!

今回作ったVerilogコード

<カメラ画像取り込み> 

/* This is a bit tricky href starts a pixel transfer that takes 3 cycles for first pixel
   then 2 cycles after 2nd pixcel

        Input | state after clock tick
         href | wr_hold   data_in           data_out     we address address_next
   cycle -1 x | xx      xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx  x  xxxx      xxx
    cycle 0 1 | 00      xxxxxxxxRRRRRGGG xxxxxxxxxxxxxxxx  x  xxxx      addr
    cycle 1 1 | 01      RRRRRGGGGGBBBBB  xxxxxxxxRRRRRGGG  x  addr      addr
    cycle 2 1 | 10      GGGBBBBBxxxxxxxx RRRRRGGGGGGBBBBB  1  addr      addr+1
*/

module ov7670_camera(
  input  wire pclk,
  input  wire vsync,
  input  wire href,
  input  wire [7:0] data,
  output wire [16:0] bram_addr,
  output reg [15:0] data_out,
  output reg  we
);

  reg [16:0] address;      // address with one clcok cyle delayed
  reg [16:0] address_next;
  reg [15:0] data_in;
  reg [1:0]  wr_hold;

  assign bram_addr = address;

  always @(posedge pclk)
    begin
     if (vsync)
       begin
         address <= 17'd0;
         address_next <= 17'd0;
         wr_hold <= 2'd0;
       end
     else
       begin
         data_out <= data_in;
         address <= address_next;
         we       <= wr_hold[1];
         wr_hold  <= {wr_hold[0],  (href & ~wr_hold[0])};
         data_in  <= {data_in[7:0], data};
         if (wr_hold[1] == 1)
           begin
             address_next <= address_next + 1;
           end
       end
    end
endmodule

<VGA画面表示> 

 module qvga_to_vga(
  input  wire clk25,
  input  wire resetN,
  output reg  [4:0] vo_r_data,
  output reg  [5:0] vo_g_data,
  output reg  [4:0] vo_b_data,
  output wire vo_hsync,
  output wire vo_vsync,
  output reg [16:0] frame_addr,
  input  wire [15:0] frame_pixel
  );
  
  parameter hRez = 640, hStartSync = 640+16, hEndSync = 640+16+96, hMaxCount = 800;
  parameter vRez = 480, vStartSync = 480+10, vEndSync = 480+10+2,  vMaxCount = 525;
  parameter hDispStart = 160, hDispEnd = 160+320;
  parameter vDispStart = 120, vDispEnd = 120+240;
  
  reg [9:0] hCounter;
  reg [9:0] vCounter;
  reg blank;

  assign vo_hsync = ((hCounter > hStartSync) && (hCounter <= hEndSync))? 0: 1;
  assign vo_vsync = ((vCounter >= vStartSync) && (vCounter < vEndSync))? 0: 1;

  always @(posedge clk25)
    begin
      if (resetN == 0)
        begin
          hCounter <= 10'd0;
          vCounter <= 10'd0;
          frame_addr <= 17'd0;
          blank <= 1;
        end
      else if (hCounter == (hMaxCount - 1) )
        begin
          hCounter <= 10'd0;
          if (vCounter == (vMaxCount - 1))
            begin
              vCounter <= 10'd0;
            end
          else
            begin
              vCounter <= vCounter + 1;
            end
        end
      else
        begin
          hCounter = hCounter + 1;
        end
        
      if (vCounter >= vDispEnd)
        begin
          frame_addr <= 0;
        end
      else if ( ((vCounter >= vDispStart) && (vCounter < vDispEnd)) && ((hCounter >= hDispStart) && (hCounter < hDispEnd)) )
        begin
          blank <= 0;
        end
      else
        begin
          blank <= 1;
        end

      if (blank == 0)
          begin
            vo_r_data <= frame_pixel[15:11];
            vo_g_data <= frame_pixel[10:5];
            vo_b_data <= frame_pixel[4:0];
            frame_addr <= frame_addr + 1;
          end
        else
          begin
            vo_r_data <= 5'd0;
            vo_g_data <= 6'd0;
            vo_b_data <= 5'd0;
          end
    end
      
endmodule

2016/12/11追記:ピン配置の制約ファイルを追記します。

set_property PACKAGE_PIN V20 [get_ports iic_0_scl_io]
set_property PACKAGE_PIN W19 [get_ports iic_0_sda_io]
set_property PACKAGE_PIN W18 [get_ports xclk]
set_property PACKAGE_PIN T20 [get_ports href]
set_property PACKAGE_PIN U20 [get_ports pclk]
set_property PACKAGE_PIN Y19 [get_ports vsync]
set_property PACKAGE_PIN M14 [get_ports led0]
set_property PACKAGE_PIN V15 [get_ports {data[0]}]
set_property PACKAGE_PIN W15 [get_ports {data[2]}]
set_property PACKAGE_PIN T11 [get_ports {data[4]}]
set_property PACKAGE_PIN T10 [get_ports {data[6]}]
set_property PACKAGE_PIN W14 [get_ports {data[1]}]
set_property PACKAGE_PIN Y14 [get_ports {data[3]}]
set_property PACKAGE_PIN T12 [get_ports {data[5]}]
set_property PACKAGE_PIN U12 [get_ports {data[7]}]
set_property PACKAGE_PIN M19 [get_ports {vo_r_data[0]}]
set_property PACKAGE_PIN L20 [get_ports {vo_r_data[1]}]
set_property PACKAGE_PIN J20 [get_ports {vo_r_data[2]}]
set_property PACKAGE_PIN G20 [get_ports {vo_r_data[3]}]
set_property PACKAGE_PIN F19 [get_ports {vo_r_data[4]}]
set_property PACKAGE_PIN H18 [get_ports {vo_g_data[0]}]
set_property PACKAGE_PIN N20 [get_ports {vo_g_data[1]}]
set_property PACKAGE_PIN L19 [get_ports {vo_g_data[2]}]
set_property PACKAGE_PIN J19 [get_ports {vo_g_data[3]}]
set_property PACKAGE_PIN H20 [get_ports {vo_g_data[4]}]
set_property PACKAGE_PIN F20 [get_ports {vo_g_data[5]}]
set_property PACKAGE_PIN P20 [get_ports {vo_b_data[0]}]
set_property PACKAGE_PIN M20 [get_ports {vo_b_data[1]}]
set_property PACKAGE_PIN K19 [get_ports {vo_b_data[2]}]
set_property PACKAGE_PIN J18 [get_ports {vo_b_data[3]}]
set_property PACKAGE_PIN G19 [get_ports {vo_b_data[4]}]
set_property PACKAGE_PIN P19 [get_ports vo_hsync]
set_property PACKAGE_PIN R19 [get_ports vo_vsync]

set_property IOSTANDARD LVCMOS33 [get_ports iic_0_scl_io]
set_property IOSTANDARD LVCMOS33 [get_ports iic_0_sda_io]
set_property IOSTANDARD LVCMOS33 [get_ports xclk]
set_property IOSTANDARD LVCMOS33 [get_ports led0]
set_property IOSTANDARD LVCMOS33 [get_ports vsync]
set_property IOSTANDARD LVCMOS33 [get_ports href]
set_property IOSTANDARD LVCMOS33 [get_ports pclk]
set_property IOSTANDARD LVCMOS33 [get_ports {data[7]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[6]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_b_data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_b_data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_b_data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_b_data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_b_data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[5]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_g_data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_r_data[4]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_r_data[3]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_r_data[2]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_r_data[1]}]
set_property IOSTANDARD LVCMOS33 [get_ports {vo_r_data[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports vo_hsync]
set_property IOSTANDARD LVCMOS33 [get_ports vo_vsync]

set_property SLEW FAST [get_ports {vo_b_data[4]}]
set_property SLEW FAST [get_ports {vo_b_data[3]}]
set_property SLEW FAST [get_ports {vo_b_data[2]}]
set_property SLEW FAST [get_ports {vo_b_data[1]}]
set_property SLEW FAST [get_ports {vo_b_data[0]}]
set_property SLEW FAST [get_ports {vo_g_data[5]}]
set_property SLEW FAST [get_ports {vo_g_data[4]}]
set_property SLEW FAST [get_ports {vo_g_data[3]}]
set_property SLEW FAST [get_ports {vo_g_data[2]}]
set_property SLEW FAST [get_ports {vo_g_data[1]}]
set_property SLEW FAST [get_ports {vo_g_data[0]}]
set_property SLEW FAST [get_ports {vo_r_data[4]}]
set_property SLEW FAST [get_ports {vo_r_data[3]}]
set_property SLEW FAST [get_ports {vo_r_data[2]}]
set_property SLEW FAST [get_ports {vo_r_data[1]}]
set_property SLEW FAST [get_ports {vo_r_data[0]}]
set_property SLEW FAST [get_ports vo_hsync]
set_property SLEW FAST [get_ports vo_vsync]
set_property SLEW FAST [get_ports xclk]

set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets pclk_IBUF]
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets pclk_IBUF_BUFG]

ZYNQ PSを使ったカメラモジュールの初期化部分はコードが長いのでGitHubに置いておきました(初期化パラメーターはmbedの作例を流用)。

Image may be NSFW.
Clik here to view.
ZYBO OV7670

その他

実は、画像は表示できているのですが、画面が鏡に映った形で左右逆になるのと画面が倒立してしいます(aitendoのシルク印刷部分を上にすると画像が倒立する)。そのため、MVEPレジスタのMirror ImageをOn及びVFlipをOnにして反転を回避しています。この点はちょっと謎です。

カメラから読み出したデーターは、カメラのPCLK(Pixel Clock)をBRAMのクロックとして使っています。BRAMのWE信号もPCLKに同期して生成しているため、BRAMクロックの立ち上がりとWEの立ち上がりが同じタイミングになり、WEのSetup Timeマージンが取れていなのではないかと思い、BRAMのクロックを100MHzのFPGAクロックにしてみました(BRAMクロックの立ち上がりエッジでWEを確実に補足するため)。そうすると、逆に若干画像にノイズが乗るような現象が見えたので、BRAMの書き込み側はPCLKに戻しています。

Next Step

ZYBO + OV7670でI2Cを動かすのにもえらく時間がかかりましたが、最初AIX4で遠回りをしたことや、カメラモジュールの不調もありましたが、画像が出せるまで1ヶ月近くかかってしまいました。まだまだ、未熟で、先人の方には数週遅れで同じことをやっていますが、次はVGAのフル解像度データーをDDR3-SDRAMをVRAMとして取り込めるようにしたいと思っています。

その次は、HLSを使って画像処理などのフィルターを挿入できるようにしてみたいと思っています。

参考資料

Vivado Integrated Logic Analyzer(ILA)の使い方

ちょっと小ネタですが、VivadoのIntegrated Logic Analyzer(ILA)を使ったFPGA内の信号観測の方法について記載します。

ILAの使い方は、以下のWebにも詳しい使い方が書いてあるのですが、最新版のVivadoではもっと簡単に設定ができることが分かったのでその内容を記載します(執筆時点ではVivado 2016.3で動作確認しています)。

1. Debug対象信号の指定

マウスの右クリックからMark Debugを選択します。選択した信号にDebugマークがつきます。信号を選択し終わったら、Run connection automationでILAを接続します。以下のように、Debugマークがついた信号がILAに接続されます。

Image may be NSFW.
Clik here to view.
Block Design with ILA

2. 論理合成

Mark DebugをつけたBlock Designで”Run Synthesis”を実行

3. Implementation実行とBitstream生成

通常の手順で”Run Implementation”と”Generate Bitstream”を実行します。

4. デバッグ画面の表示

Hardware Managerを開き、”Open target”をクリック。

Image may be NSFW.
Clik here to view.
Hardware Mnager

Auto Connectをクリック。ターゲットが認識されるので、”Program device”でbitstreamをダウンロードします

Image may be NSFW.
Clik here to view.
Bitstream download
ˇ

ZynqデバイスのFCLK_CLK0など、PSからのクロックをロジックアナライザに使用している場合やPSのプログラムで周辺デバイスの初期化などを行なっている場合はは、Xilinx SDKでプログラムを実行してPSを起動します。

その後、”Refresh device”を実行します。デバッグ画面が表示されます。 Waveformウインドウにデバッグ対象にした信号の一覧が表示されています。

Image may be NSFW.
Clik here to view.
Debug Screen

5. デバッグ

トリガーにしたい信号をWaveformウインドウからTrigger Setupウインドウにドラッグアンドドロップし、トリガー条件を設定します。

Image may be NSFW.
Clik here to view.
Trigger Setup

”Run Trigger for this ILA core”ボタンを押すとトリガー条件を検出時に信号が波形表示されます。

Image may be NSFW.
Clik here to view.
Wave From Display

以前のように制約ファイル(xdcファイル)にデバッグ用のエントリを追加する必要もありません。デバッグが終わったらデバッグ対象の信号を右クリックして”Clear Debug”を実行し、ILAを削除すれば終わりです。

非常にお手軽に信号観測ができます。

ZYBOでOV7670カメラモジュールのVGA画像を表示する

前回行った、OV7670カメラモジュールを使ったQVGA画像の表示を拡張して、VGA画像を表示できるようにしてみました。まだ不完全な部分がありますが、やったことを書いてみます。

全体構想

VGA(640 x 480) x 16bitカラーの画像を扱うためには、600KBのVRAM容量が必要となり、QVGAのようにFPGAのBRAMには格納できないため、ZYBOのPS側に搭載されているDDR3 RAMをVRAMとして使用する必要があります。今回はPL(FPGA部)からDDR3 RAMへのアクセスを行うことがテーマでした。

やり方は以下の3通りが考えられますが、③のAXI-HPを使ったメモリアクセスを使いました。

  1. AXIGPポート経由でアクセス(32bit幅)
  2. ZYNQ DMAエンジンを使ってDMA転送
  3. AXIHPポート経由でアクセス(32/64bit幅)

AXIHPポートを使ったDDR3メモリアクセスのイメージを下記に示します。データー転送側がAXI Masterデバイスとなります。

Image may be NSFW.
Clik here to view.
AXIHP

DDR3アクセスのために、AXI Masterプロトコルを喋るモジュールを作る必要がありますが、HLSで作るのが一番楽だと考え、以下のような構成としました。(インタフェースポートの属性にm_axiを指定するだけであとはHLSがAXI Masterの処理を行なってくれるため)

Image may be NSFW.
Clik here to view.
System_Design

Cameraからのデータ受信モジュールとVGA IFはクロックに従ったデーターの送受信が必要なため、HLSではなくVerilogで作成しています(前回作ったモジュールを拡張)。Verilog - HLS間のインタフェースはAXIS(Stream)を使うことも考えたのですが、どのタイミングでAXISのデーターの切れ目(tvalid)をHLSに伝えればよいかがよく分からなかったため、ピクセル座標をap_hs(ハンドシェーク)で渡すインタフェースとしました。当初はap_hsもそれほど苦労せずに作れるだろうと思ったのですが、ap_hsを動かすのに思いっきりハマりました。

今回の構成では、カメラからのデータの取り込みはOV7670のPCLK(24MHz)に同期する必要がありますが、メモリアクセス部分はFPGA PLに供給されている100MHzのシステムクロック(FCLK)で動作するため、クロックドメインまたがり(Clock Domain Crossing: CDC)の問題に直面し、このせいで(だと思うのですが)当初Cameraデーター受信モジュールが期待通りに動いてくれずかなり悩みました。あまりスマートでないように思うのですが、カメラ側の24MHzクロックをFCLKに同期化することで対処しています。

各モジュールのコード

HLSで書いたMemory Write / Memory Readジュールのコードは以下の通りです。

<Memory Write> 

#include <ap_int.h>
#include <stdint.h>

#define MAXLINE		480
#define MAXCOL		640

void MemWrite(ap_uint line, ap_uint col, uint16_t data, uint64_t *VRAM)
{
// depth 1280byte of lineBuff size is too big (got SIGSEGV fault in C/RTL co-sim),
// but 160 words(64bit) is too small
#pragma HLS INTERFACE m_axi depth=640 port=VRAM offset=direct bundle=VRAMW
#pragma HLS INTERFACE ap_hs port=col
#pragma HLS INTERFACE ap_hs port=line
#pragma HLS INTERFACE ap_hs port=data
#pragma HLS INTERFACE ap_ctrl_hs port=return

	static uint64_t lineBuff[2][MAXCOL/4];
// Force to make 2 line buffers in different BRAM
#pragma HLS ARRAY_PARTITION variable=lineBuff factor=2 dim=1
#pragma HLS RESOURCE variable=lineBuff core=RAM_1P_BRAM
	static uint64_t strData = 0;
	static ap_uint<2> mod = 0;
	ap_uint<1> cur;
	uint64_t inData;


// Data reception block
	cur    = line & 0x0001;
	inData = data;

	switch(mod) {
		case 0: strData = data;
				break;
		case 1: strData = strData | inData << 16;
				break;
		case 2: strData = strData | inData << 32;
				break;
		case 3: strData = strData | inData << 48;
				lineBuff[cur][col/4] = strData;
				break;
	}
	mod++;

	if (col == (MAXCOL - 1)) {
		// Data transfer block
		uint64_t* destPtr = &VRAM[line*MAXCOL/4];
		memcpy(destPtr, lineBuff[cur], MAXCOL*2);
		mod = 0;
		return;
	}

	return;
}

<Memory Read>

#include <ap_int.h>
#include <stdint.h>

#define MAXLINE		480
#define MAXCOL		640

uint16_t MemRead(ap_uint vga_line, ap_uint vga_col, uint64_t* VRAM)
{

#pragma HLS INTERFACE m_axi depth=640 port=VRAM offset=direct bundle=VRAMR
#pragma HLS INTERFACE ap_hs port=vga_line
#pragma HLS INTERFACE ap_hs port=vga_col
#pragma HLS INTERFACE ap_ctrl_hs port=return

	static uint64_t lineBuff[2][MAXCOL/4];
// Force to make 2 line buffers in different BRAM
#pragma HLS ARRAY_PARTITION variable=lineBuff factor=2 dim=1
#pragma HLS RESOURCE variable=lineBuff core=RAM_1P_BRAM

	static uint64_t rdData = 0;
	static ap_uint<2> mod = 0;
	ap_uint<1> cur;
	uint16_t pixcelData;

	cur = vga_line & 0x0001;

	// Transfer data during blanking period
	if (vga_col == 0) {
		uint64_t* srcPtr = &VRAM[vga_line*MAXCOL/4];
		memcpy(lineBuff[cur], srcPtr, MAXCOL*2);
		mod = 0;
	}

	switch(mod) {
		case 0: rdData = lineBuff[cur][vga_col/4];
				pixcelData = (uint16_t)rdData;
				break;
		case 1: pixcelData = (uint16_t)(rdData >> 16);
				break;
		case 2: pixcelData = (uint16_t)(rdData >> 32);
				break;
		case 3: pixcelData = (uint16_t)(rdData >> 48);
				break;
	}
	mod++;

	return pixcelData;
}

内容的には単純な処理で、MemoryWriteはCameraモジュールからCol/Lineの座標データと16bitカラーのデーターを受信し、4ピクセル分のデータを64bit長の配列に格納します。1 Line分(640 pixel)のデータを受信すると水平同期のブランキング期間を使ってデータをDDR3メモリーにバースト転送します。データー転送の時間を短くするためAXIバスは64bit長で動かしています(そのため、ピクセルデーターを64bit単位にパッキングしています)。VRAM portに64bit符号なし整数に対するポインターを(uint64_t *)指定して、インタフェースモードにm_axiを指定すると64bitモードで動いてくれました。

VRAMメモリのオフセット値はdirectを使ってBlock Designのconstantで指定しています(今回は、80_0000Hを使用)。

MemoryReadも同様で、水平同期のブランキング期間を最初に持ってきており、1 Line分のデータをブランキング期間にDDRメモリから読み込んで、VGA IFが要求するpixcel座標に対応したデーターを渡すようにしています。

HLSの合成結果を以下に示します。MemWriteは最小2クロックサイクルで動作し、DDRメモリーにデーター転送を行う際は169クロックかかることが分かります。MemReadは同様な処理を行なっているのですがLatencyが1クロックサイクル多く、1処理に3クロックサイクルを要しています。

Image may be NSFW.
Clik here to view.
MemWrite

Image may be NSFW.
Clik here to view.
MemRead

次に、OV7670カメラモジュールIF、VGA IFのVerilogコードを示します。以前のHDLコードは参考にしたブログのコードそのままなのですが、複数のレジスターを一つのalwaysブロックの中で操作していました。参考書として参照した「FPGAボードで学ぶ組込みシステム開発入門[Altera編] 」ではレジスタ毎に独立したalwaysブロックを使うスタイルの記述になっていたため、参考書のスタイルに改めました。生成される回路は異なると思いますが、alwaysブロックで複数レジスタを操作しないのがベストプラクティスな書き方なのかがまだよく分かっていません。

<OV7670カメラモジュールIF>

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer:  Kenshi Kamiya
//
// Create Date: 2016/10/11 15:00:13
// Design Name: ov7670_camera
// Module Name: ov7670_camera
// Project Name: ov7670 camera data capture and output data wht ap_hs
// Target Devices: Zybo
// Tool Versions: Vivado 2016.3
//
// Revision: 01
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////

/* This is a bit tricky href starts a pixel transfer that takes 3 cycles for first pixel
   then 2 cycles after 2nd pixcel

        Input | state after clock tick
         href | wr_hold   data_in           data_out     we   col     col_next
   cycle -1 x | xx      xxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxx  x  xxxx      xxx
    cycle 0 1 | 00      xxxxxxxxRRRRRGGG xxxxxxxxxxxxxxxx  x  xxxx      col
    cycle 1 0 | 01      RRRRRGGGGGBBBBB  xxxxxxxxRRRRRGGG  x  col       col
    cycle 2 x | 10      GGGBBBBBxxxxxxxx RRRRRGGGGGGBBBBB  1  col       col+1
*/

module ov7670_camera_hs(
  input  wire clk,
  input  wire resetN,
  input  wire pclk,
  input  wire vsync,
  input  wire href,
  input  wire [7:0] data,
  output reg  [9:0] line,
  output reg  [9:0] col,
  output reg  [15:0] data_out,
  output wire  ap_start,
  input  wire ap_idle,
  input  wire ap_done,
  output wire  col_vld,
  output wire  line_vld,
  output wire  data_vld,
  input  wire col_ack,
  input  wire line_ack,
  input  wire data_ack
);

  reg [9:0] col_next;
  reg [9:0] line_next;
  reg [7:0]  data_s;
  reg [15:0] data_in;
  reg [1:0]  wr_hold;
  reg        we;
  reg [2:0]  state;
  reg [3:0] control;
  // Registers for synchronizer
  reg       pclk_d, pclk_s;
  reg       href_d, href_s;
  
  parameter IDLE=0, AP_START=1, OUT_VLD=2, OUT_ACK=3, AP_DONE=4, WAITE_NEXT=5;
  parameter MAXCOL=640, MAXLINE=480;
  
  // Synchronizer for pclk
  always @(posedge clk)
    begin
      pclk_d <= pclk;
      pclk_s <= pclk_d;
    end

  // Synchronizer for href
  always @(posedge clk)
    begin
      href_d <= href;
      href_s <= href_d;
    end
  
  // Synchronizer for input data
  always @(posedge clk)
    begin
      data_s <= data;
    end
  
  // FSM for ap_hs protocol
  always @(posedge clk)
    if (resetN == 0)
      state <= IDLE;
    else
      begin
        case(state)
          IDLE:     begin
                      if (we == 1)
                        begin
                          state <= AP_START;
                        end
                      else
                        begin
                          state <= IDLE;
                        end
                    end
          AP_START: begin
                      state <= OUT_VLD;
                    end
          OUT_VLD:  begin
                      state <= OUT_ACK;
                    end
          OUT_ACK:  begin
                      // ap_ack retun immediatry, so no chcke ap_ack
                      // To catch when ap_done return one clclk after ap_ack
                      if (ap_done == 1)
                        begin
                          state <= WAITE_NEXT;
                        end
                      else
                        begin
                          state <= AP_DONE;
                        end
                    end
          AP_DONE:  begin
                      if (ap_done == 1)
                        begin
                          state <= WAITE_NEXT;
                        end
                      else
                        begin
                          state <= AP_DONE;
                        end
                    end
          WAITE_NEXT: begin
                        // Wait to end current WE cycle
                        if(we == 1)
                          begin
                            state <= WAITE_NEXT;
                          end
                        else
                          begin
                            state <= IDLE;
                          end
                      end
          default:    begin
                          state <= IDLE;
                      end
        endcase
      end

  // Generate control signal
  always @(*)
    case(state)
      IDLE:     control = 4'b0000;
      AP_START: control = 4'b1000;
      OUT_VLD:  control = 4'b1111;
      OUT_ACK:  control = 4'b0000;
      AP_DONE:  control = 4'b0000;
      WAITE_NEXT: control = 4'b0000;
      default:  control = 4'b0000;
    endcase

  assign {ap_start, col_vld, line_vld, data_vld} = control;
  

  //  Camera data capture FSM
  always @(posedge pclk_s)
    begin
      if (vsync)
        wr_hold  <= 2'd0;
      else
        begin
          we <= wr_hold[1];
          wr_hold  <= {wr_hold[0],  (href_s & ~wr_hold[0])};
          data_out <= data_in;
          data_in  <= {data_in[7:0], data_s};
        end
    end

  always @(posedge pclk_s)
    begin
      if (vsync)
        begin
          col      <= 10'd0;
          col_next <= 10'd0;
        end
      else
        begin
          col <= col_next;
          if (wr_hold[1] == 1)
            begin
              col_next <= col_next + 1;
              if (col_next == MAXCOL - 1)
                 col_next  <= 10'd0;
            end
        end
    end

  always @(posedge pclk_s)
    begin
      if (vsync)
        begin
          line     <= 10'd0;
          line_next<= 10'd0;
        end
      else
        begin
          line <= line_next;
          if (wr_hold[1] == 1 && col_next == MAXCOL - 1)
              begin
                line_next <= line_next + 1;
                if (line_next == MAXLINE - 1)
                    line_next <= 10'd0;
              end
          end
        end

endmodule

<VGA IF> 

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
// Company:
// Engineer: Kenshi Kamiya
//
// Create Date: 2016/11/06 16:33:59
// Design Name: VGA Driver with ap_hs protocol
// Module Name: vga_hs
// Project Name: OV7670_VGA
// Target Devices: Zybo
// Tool Versions: Vivado 2016.3
// Description:
//
// Dependencies:
//
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
//
//////////////////////////////////////////////////////////////////////////////////

module vga_hs(
  input wire  clk,
  input  wire clk25,
  input  wire resetN,
  output reg  [4:0] vo_r_data,
  output reg  [5:0] vo_g_data,
  output reg  [4:0] vo_b_data,
  output reg  vo_hsync,
  output reg  vo_vsync,
  output reg [9:0] vga_line,
  output reg [9:0] vga_col,
  input  wire [15:0] frame_pixel,
  output wire ap_start,
  input  wire ap_idle,
  input  wire ap_done,
  output wire line_vld,
  output wire col_vld,
  input  wire line_ack,
  input  wire col_ack
  );
  
  parameter hRez = 640, hFrontPorch = 16, hSyncPluse = 96, hBackPorch = 48, hMaxCount = 800;
  parameter vRez = 480, vFrontPorch = 10, vSyncPluse = 2,  vBackPorch = 33, vMaxCount = 525;
  parameter hDispStart = hFrontPorch + hSyncPluse + hBackPorch;  // 160
  parameter vDispStart = vFrontPorch + vSyncPluse + vBackPorch;  // 45
  
  parameter IDLE=0, AP_START=1, OUT_VLD=2, AP_DONE=3, WAITE=4;
  
  reg  [9:0] hCounter;
  reg  [9:0] vCounter;
  reg  [9:0] cur_hCounter;
  reg   blank;
  reg  [2:0]  state;
  reg  [15:0] pixelData;
  reg  [2:0] control;
  wire cke;
  
  assign cke   = ~blank & clk25;

  // hCounter
  always @(posedge clk25)
    begin
      if (resetN == 0)
        hCounter <= 10'd0;
      else if (hCounter == (hMaxCount - 1))
        hCounter <= 10'd0;
      else
        hCounter <= hCounter + 1;
     end

  //vCounter
  always @(posedge clk25)
    begin
      if (resetN == 0)
        vCounter <= 10'd0;
      else if (hCounter == (hMaxCount - 1))
        if (vCounter == (vMaxCount - 1))
          vCounter <= 10'd0;
        else
          vCounter <= vCounter + 1;
    end

  // FSM for ap_hs protocol
  always @(posedge clk)
    if (resetN == 0)
      state <= 0;
    else
      case(state)
        IDLE:     begin   // State 0
                    if ( (cke && vga_col >= 1) || (vCounter >= vDispStart && hCounter == 11'd10))
                      state = AP_START;
                    else
                      state = IDLE;
                  end
        AP_START: begin   // State 1
                    state = OUT_VLD;
                  end
        OUT_VLD:  begin   // State 2
                    state = AP_DONE;
                  end
        AP_DONE:  begin   // State 3
                    // ap_ack retun immediately, so no chcke ap_ack
                    if (ap_done && cke)
                      state <= AP_START;    // Sthort cut to execute next cycle
                    else if (ap_done )
                      state = IDLE;
                    else if (ap_done && hCounter < hDispStart)
                      state = WAITE;
                    else
                      state= AP_DONE;
                  end
        WAITE:    begin   // State 4
                    // Waite to finish hBlanking in case read first block at line start
                    if (hCounter < hDispStart)
                      state = WAITE;
                    else
                      state = IDLE;
                  end
        default:  begin
                    state = IDLE;
                  end
      endcase

  always @(*)
    case(state)
      IDLE:     control = 3'b000;
      AP_START: control = 3'b100;
      OUT_VLD:  control = 3'b111;
      AP_DONE:  control = 3'b000;
      WAITE:    control = 3'b000;
      default   control = 4'b000;
    endcase

  assign  {ap_start, line_vld, col_vld} = control;
  
  // Generate HSYNC
  always @(posedge clk25)
  begin
    if (resetN == 0)
      vo_hsync <= 1;
    else if ((hCounter > hFrontPorch) && (hCounter <= hFrontPorch + hSyncPluse))
      vo_hsync <= 0;
    else
      vo_hsync <= 1;
  end

  // Generate VSYNC
  always @(posedge clk25)
  begin
    if (resetN == 0)
      vo_vsync <= 1;
    else if ((vCounter > vFrontPorch) && (vCounter <= vFrontPorch + vSyncPluse))
      vo_vsync <= 0;
    else
      vo_vsync <= 1;
  end
  
  // Generate blank signal
  always @(posedge clk25)
  begin
    if (resetN == 0)
      blank <= 1;
    else if ((vCounter >= vDispStart) && (vCounter < vMaxCount ) && (hCounter >= hDispStart) && (hCounter < hMaxCount) )
      blank <= 0;
    else
      blank <= 1;
  end

  // VGA column address
  always @(posedge clk25)
    begin
      if (resetN == 0)
        vga_col  <= 10'd0;
      else if (blank == 0)
       if (vga_col == hRez - 1)
          vga_col  <= 10'd0;
        else
          vga_col <= vga_col + 1;
  end

  // VGA line address
  always @(posedge clk25)
    begin
      if (resetN == 0)
        vga_line <= 10'd0;
      else if (blank == 0)
        if (vga_col == (hRez - 1))
          if (vga_line == (vRez -1))
            vga_line <= 10'd0;
          else
            vga_line <= vga_line +1;
  end

  // Generate color signal
  always @(posedge clk25)
    begin
      if (blank == 0)
        begin
          vo_r_data <= pixelData[15:11];
          vo_g_data <= pixelData[10:5];
          vo_b_data <= pixelData[4:0];
        end
      else
        begin
          vo_r_data <= 5'd0;
          vo_g_data <= 6'd0;
          vo_b_data <= 5'd0;
        end
  end

  always @(posedge ap_done)
    begin
      pixelData <= frame_pixel;
    end

endmodule

動作確認(シミュレーション)

Verilogで作成したCamera IFとHLSで作成したMemWriteが正しく動作するかの検証を行いました。特にap_hsプロトコルが期待通りに動くかの検証が必要でした。ただ、ov7670_camera_hsとMemWriteをBlock Designで繋いだだけではダメで、MemWriteをAXI-Slaveに繋いでやらないと動作してくれません。AXIのBFMが有償なのでシミュレーションができないとTwitterでぼやいていたら、@marsee101 さんからご自身で作成されたAXI Slave BFMのありかを教えていたき、そのBFMを使って以下のようなシミュレーション環境を作りました。

Image may be NSFW.
Clik here to view.
MemWrite_TsetBed

axi_slave_bfmが @marsee さんから拝借したAXI-SlaveのBFMです。おかげで、VerilogとHLSモジュールの連携をシミュレーションすることができて大変助かりました。Block DesignでMemWrite (HLS) とaxi_bfm_slaveを作成してauto connectを実行すると、自動的にAXI-InterconnectやProsessor System Resetモジュールもインスタンス化され接続してくれました。

当初のコードでは、ov7670_camera_hsの164行目に相当する、OV7670からのデーター受信処理で、OV7670からのpclkをそのまま使ってpclkに同期したwe信号(ov7670_camera_hs 170行目)がHighになることをトリガーにして、ap_hsをスタートして(ov7670_camera_hs 94行目)MemWrite (HLS)にデーター転送を行おうとしました。このコードでは、シミュレーションではBehavioral Simulation、Post-Synthesis Simulation共に正常に動作しているように見えたのですが、実機ではどうしてもweがHighになった際にap_startパルスが出ず、MemWriteを起動できない問題に遭遇しました。

この問題の解決にかなり時間を要したのですが、we信号はpclkに同期して生成しているが、ap_hsプロトコルはpclkとは非同期の100MHz FCLKに同期して動かしているため、クロックドメインの違いによって問題が発生しているのではないかと考え、pclkを67行目に示すシンクロナイザーを通してFCLKに同期化してやると正常に動作するようになりました。

クロックドメインまたがり(CDC: Clock Domain Crossing)の処理として、入力クロックをシステムクロックで打ち直すのが正しい処理なのか自信がないのですが、まずは動いているのでよしとしています。Post-Synthesis Timing Simulationを行なった際の波形を以下に示します。

Image may be NSFW.
Clik here to view.
MemWrite Simulation-1

href信号が有効になると画像データーの取り込み処理を始めて、weがHighになった次のシステムクロック(clk)の立ち上がりエッジでap_startを出し、3クロックサイクル後にap_doneが返っています。ap_startの信号を組み合わせ回路で生成しているため、信号にグリッチが出ています。レジスターを通すことでグリッチをなくすことができるのですが、ap_startの立ち上がりが1クロック遅延します。Camera IFではタイミングに余裕があるためレジスターを入れてもよいのですが、後で説明するVGA IF回路ではタイミングの余裕がなく、制御信号生成にレジスターを入れることができなかったため、処理ロジックを共通化する意味で、信号のグリッチには目をつぶっています(動作には問題なさそうですので)。

また、これまでFSMを書く際は、状態遷移(next_stateに遷移する条件)を組み合わせ回路(always @*)で記述して、状態を進める処理を以下のような順序回路で記述していました: 

always @(posedge clk)
  begin
    if (resetN == 0)
      cur_state <= IDLE;
    else
      cur_state <= next_state;
  end 

このような組み合わせ回路と状態を進める順序回路の組み合わせでFSMを記述すると、weを検出した際のap_start起動に1クロックの遅延が発生します。VGA IF回路ではap_startの遅延を最小限にしたかったため、今回は状態遷移自体を順序回路のスタイルで記述しています。

640 column分のデータを受信すると(colアドレス = 0x27Fになると)MemWrite (HLS)がDDRメモリーにデーター転送処理を始めるためap_doneが直ぐに返らずに、169クロック後にap_doneが返ってきます。

Image may be NSFW.
Clik here to view.
MemWrite Simulation-2

Image may be NSFW.
Clik here to view.
MemWrite Simulation 3

VGA IFに関しても同様の試験環境を作ってシミュレーションを行なっています。

Image may be NSFW.
Clik here to view.
MemRead TestBed

OV7670 Camera IFの場合は24MHzサイクルの2クロック毎にMemWriteモジュールにデーターを転送できればよいため比較的タイミングに余裕があったのですが、VGA IFでは25MHzのVGA pixcelクロック毎にMemReadモジュールから画像データーをもらう必要があるため、タイミングマージンがギリギリになりました。以下にPost-Synthesis Timing Simulationを行なった際の波形を示します。VGA IFではvga_hsの59行目でvga pixcelクロック(25MHz)と有効画素範囲(ブランキング期間以外)のANDを取ってこのcke信号がHighになることをトリガーにしてMemReadモジュールに対してap_startを出しています。

Image may be NSFW.
Clik here to view.
MemRead Simulation

ap_startを最短で検出できるよう、VGA clockをMMCMで生成する際に-30度位相シフトして、VGA clk(clk25)がHighになった直後のシステムクロック(clk)の立ち上がりでap_startが出せるようにしています。シミュレーションでは次のVGAクロックサイクルが始まる直前ギリギリにap_doneが返っていますが、やはりpixelデーター毎にハンドシェークを行うap_hsを使うのはタイミング的に厳しいので、このようなケースではAXIS(ストリーミング)を使った方がよいように思われます。

全体の構成

全モジュールを結合した最終的なBlock Designを以下に示します。

Image may be NSFW.
Clik here to view.
System Block Design

Cameraモジュールでap_startが出ない問題にかなり悩まされましたが、なんとか動くようになりました。

残課題

以下の課題が残っており、まだ完全とは言えません。

1) 画像の左側にノイズが出る

OV7670カメラの水平方向の有効画素範囲の設定の問題だと思うのですが、該当レジスタ値(HSTART, HSTOP, HREF)を変更して表示ウインドウを右にシフトさせようとしたのですが、デフォルト値以外の値に設定すると、href信号の出力パターン(パルス幅)が大きく変わってしまい画像が取り込めなくなるため、現状未解決です。

Image may be NSFW.
Clik here to view.
Lift side noize

2) タイミング制約の問題

ov7670_camera_hsモジュールで、システムクロックに同期化したpclk_sを使って生成している信号やレジスタが、合成後unconstrainedになっています。unconstrainedの警告を消すために、試しにpclk-sにクロック制約を入れると、今度は合成時にhold/setupマージン不足(negative slack)の状態になるクロックパスが多数発生してしまうため、クロック制約は外しています。タイミング制約の使い方やタイミング条件の正しい設計はもっと勉強をしないといかんです。

クロック制約:create_clock -period 41.666 -name pclk_s -waveform {0.000 20.833} [get_pins ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s_reg/Q]

Image may be NSFW.
Clik here to view.
Unconstrained

3) Unknown 1-bit CDC  circuitry

クロック載せ替えで問題が出ていたので、Report CDCを行うと、1-bit unknown CDC circuitryのパスが多数検出されます。

Image may be NSFW.
Clik here to view.
Unkonown 1bit CDC

Unsafeではないのですが、1-bit unknown CDCが出ている回路を表示してみると、以下のように、システムクロックに同期したリセット信号と、同じくシステムクロックに同期したVGAクロックが同じFFに入力されている2つの信号の関係性で、unknown 1-bit CDCが発生しているように見えます。試しに、destination側のレジスタに”ASYNC_REG”プロパティーを設定したりしたのですが変化がありませんでした。クロック載せ替えの最適化もまだよく分かっていない部分です。

Image may be NSFW.
Clik here to view.
Unkonown CDC 2

参考資料

Vivado Constraints Wizardによるクロック・入出力制約の作成

前回のポストで、ZYBOを使って、OV7670カメラのVGA画像が取り込めるようになりましたが、制約条件が未設定でした。VivadoのConstraints Wizardを使うことによってクロックと入出力の制約条件を設定して、Unconstraintsの箇所を消すことができました。制約条件を設定することによって画質が向上したため、回路を正しく動作させるためには制約条件の設定は必須なんだと分かりました。

以下に、Constraints Wizardを使った制約条件の設定方法を記載します。

操作手順

Constraints WizardはVivadoのFlow Navigatorから起動でき、SynthesisとImplementationの両方から起動できます。一旦合成・配置を行なった後だと、どちらから起動しても作成される制約ファイルの内容は同じになるようです。

Image may be NSFW.
Clik here to view.
Constraints Wizard

Wizardを起動します。カメラのPCLKをFCLKで同期化したpclk_sと、HLSとのインタフェースに使っているap_doneが、Generated Clock(ユーザー定義のクロック)の候補として表示されました。ソースクロック(FCLK)の分周率を指定する形式となっており、実際のクロック周波数とは同じになりませんが、ここでは4を指定しました。pclk_sは24MHzなので、100MHz ÷ 4がそれに近い値ということで。

当初はpclk_sの制約条件を”create_clock”コマンドで作成しようとしたのですが、create_clockはプライマリークロックの定義となっているため、create_clockを使うとImplematation時にタイミング違反が発生してしまいます。このようなケースでは、”create_generated_clock”を使うのが正しいのだということが分かりました。

Image may be NSFW.
Clik here to view.
Constraints Wizard-2

Image may be NSFW.
Clik here to view.
Constraints Wizard-3

次に入力遅延を設定します。入力遅延はリファレンスクロック(ここではFCLK)の立ち上がりに対する遅延ということなので、これも実態(PCLKの立ち上がりに対して、実際のデーターはセットアップ時間前に有効になっている)とは異なりますが、1〜2 nsの遅延を指定。

Image may be NSFW.
Clik here to view.
Constraints Wizard input delay

続けて、出力遅延を設定。こちらは、25MHzのVGAクロックの立ち上がりに対するセットアップ・ホールド遅延の設定になります。

Image may be NSFW.
Clik here to view.
Constraints Wizard output delay

次に、非同期クロックドメインを指定する画面が表示されます。下段に3つのエントリが表示されていますが、Non-recommendedということなのでチェックはしていません。この項目にチェックを入れると、"set_clock_groups -asynchronous “の制約が生成されるのですが、オブジェクトに指定されるクロック名が認識できない旨のCritical Waringが合成時に出てしまいました。

Image may be NSFW.
Clik here to view.
20161211 Constraints Wizard Async Clock

最後にWizardを完了。

Image may be NSFW.
Clik here to view.
Constraints Wizard done

以下のエントリが制約ファイルに追加されました。 

create_generated_clock -name ov7670_vram_sys_i/MemRead_0/inst/ap_done -source [get_pins {ov7670_vram_sys_i/processing_system7_0/inst/PS7_i/FCLKCLK[0]}] -divide_by 4 [get_pins {ov7670_vram_sys_i/MemRead_0/inst/ap_CS_fsm_reg[10]/Q}]
create_generated_clock -name ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s -source [get_pins {ov7670_vram_sys_i/processing_system7_0/inst/PS7_i/FCLKCLK[0]}] -divide_by 4 [get_pins ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s_reg/Q]
create_clock -period 40.000 -name VIRTUAL_ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s -waveform {0.000 20.000}
create_clock -period 40.000 -name VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0 -waveform {-3.333 16.667}
set_input_delay -clock [get_clocks clk_fpga_0] -min -add_delay 2.000 [get_ports {data[*]}]
set_input_delay -clock [get_clocks clk_fpga_0] -max -add_delay 3.000 [get_ports {data[*]}]
set_input_delay -clock [get_clocks clk_fpga_0] -min -add_delay 2.000 [get_ports href]
set_input_delay -clock [get_clocks clk_fpga_0] -max -add_delay 3.000 [get_ports href]
set_input_delay -clock [get_clocks clk_fpga_0] -min -add_delay 2.000 [get_ports pclk]
set_input_delay -clock [get_clocks clk_fpga_0] -max -add_delay 3.000 [get_ports pclk]
set_input_delay -clock [get_clocks VIRTUAL_ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s] -min -add_delay 2.000 [get_ports vsync]
set_input_delay -clock [get_clocks VIRTUAL_ov7670_vram_sys_i/OV7670_camera_hs_0/inst/pclk_s] -max -add_delay 3.000 [get_ports vsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports {vo_b_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports {vo_b_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports {vo_g_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports {vo_g_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports {vo_r_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports {vo_r_data[*]}]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports vo_hsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports vo_hsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -min -add_delay -1.000 [get_ports vo_vsync]
set_output_delay -clock [get_clocks VIRTUAL_clk25_ov7670_vram_sys_clk_wiz_0_0] -max -add_delay 3.000 [get_ports vo_vsync]

合成・インプリメンテーションを実行すると、Unconstainted Pathがなくなりました。

Image may be NSFW.
Clik here to view.
Timing Summary

Clock Interactionを表示するとまだUnsafeなクロック乗せ変えパスが表示されています。WizardのAsynchronous CDCをチェックして赤がついているクロック間を非同期にするとUnsafeの表示を消すことができるのですが、先に示したように、Wizardが生成する制約エントリがうまく認識されないのでこのままとしています。 

Image may be NSFW.
Clik here to view.
Constraints Wizard clock interaction

制約条件を設定することによって、タイミング条件はかなり改善されたようです。入力側の信号がFPGAのFCLKに同期しているわけではないため(実態はカメラ側のPCLKに同期)、本当にこれでよいのかはまだよくわかっていないのですが。

結果

制約条件を加えて作成したBitstreamを使ってカメラを立ち上げると、以前に比べて画質がかなり向上しました。制約条件を入れる前は画像にジャギーが目立っていたのですが、制約条件を設定した後はジャギーがなくなりました。

<制約条件設定前の画像>

Image may be NSFW.
Clik here to view.
Constraints Wizard before

<制約条件設定後の画像>

Image may be NSFW.
Clik here to view.
Constraints Wizard after

制約条件が未設定の状態では回路が正しく動いていなかったことが分かりました。FPGAを使った回路の設計では、制約条件の設定やタイミング・クロージャーは必須事項なのだとよく分かりました。

参考資料

PYNQ-Z1のOverlay読み込みとPythonからのFPGA PLの制御

PYNQ-Z1はZYBOとよく似たZYNQ SoCを使ったFPGAボードですが、Pythonを使ってLinuxからFPGAのリソースにアクセスできることが特徴です。PYNQではFPGAのConfiguration Data (bitsteam) をOverlayと呼んでおり、標準でPYNQのI/Oやビデオ関係の処理ができるOverlayが提供されているのですが、ドキュメントを読んでいると、カスタムOverlayも作成できるとあります。

カスタムOverlayを作ってそれを動的にダウンロードできれば、FPGAのReconfigurableな利点を生かして、必要に応じてI/Oピンの割り当てを変更したり、さらには必要に応じてハードで処理する機能(画像処理など)を組み込めるという、RaspberryPiなどのマイコンボードでは絶対に真似ねできない、高い拡張性とPythonによる容易なプログラミングができるということになります。

本当にカスタムOverlayが動的にダウンロードできるのか(Linuxを動かしながらFPGAを動的にconfig変更できるのか)Lチカで実験してみました。

OverlayダウンロードとPythonからの制御の仕組みを調べる

PYNQはSMBのサーバーとして動いているため、WindowsのFile ExplorerやmacOSのFinderで/home/xilinx配下のディレクトリーやファイルにアクセスが可能です。

Image may be NSFW.
Clik here to view.
QYNQ Folder

pl.pyファイルの中にあるbitstreamクラスがOverlayのダウンロードに関係していそうです。ファイルの中身を以下に示します。 

class Bitstream(PL):
    """This class instantiates a programmable logic bitstream.
    
    Attributes
    ----------
    bitfile_name : str
        The absolute path of the bitstream.
    timestamp : str
        Timestamp when loading the bitstream. Format:
        year, month, day, hour, minute, second, microsecond
        
    """
    
    def __init__(self, bitfile_name):
        """Return a new Bitstream object.
        
        Users can either specify an absolute path to the bitstream file
        (e.g. '/home/xilinx/src/pynq/bitstream/base.bit'),
        or only a relative path.
        (e.g. 'base.bit').
        
        Note
        ----
        self.bitstream always stores the absolute path of the bitstream.
        
        Parameters
        ----------
        bitfile_name : str
            The bitstream absolute path or name as a string.
            
        """
        super().__init__()
        
        if not isinstance(bitfile_name, str):
            raise TypeError("Bitstream name has to be a string.")
        
        if os.path.isfile(bitfile_name):
            self.bitfile_name = bitfile_name
        elif os.path.isfile(general_const.BS_SEARCH_PATH + bitfile_name):
            self.bitfile_name = general_const.BS_SEARCH_PATH + bitfile_name
        else:
            raise IOError('Bitstream file {} does not exist.'\
                            .format(bitfile_name))
            
        self.timestamp = ''

    def download(self):
        """The method to download the bitstream onto PL.
        
        Note
        ----
        The class variables held by the singleton PL will also be updated.

        Parameters
        ----------
        None

        Returns
        -------
        None
            
        """
        # Compose bitfile name, open bitfile
        with open(self.bitfile_name, 'rb') as f:
            buf = f.read()
        
        # Set is_partial_bitfile device attribute to 0
        with open(general_const.BS_IS_PARTIAL, 'w') as fd:
            fd.write('0')
        
        # Write bitfile to xdevcfg device
        with open(general_const.BS_XDEVCFG, 'wb') as f:
            f.write(buf)
        
        t = datetime.now()
        self.timestamp = "{}/{}/{} {}:{}:{} +{}".format(t.year,t.month,t.day,\
                                t.hour,t.minute,t.second,t.microsecond)
        
        # Update PL information
        PL._client_request()
        PL._bitfile_name = self.bitfile_name
        PL._timestamp = self.timestamp
        PL._ip_dict = {}
        PL._gpio_dict = {}
        PL._server_update()

64〜65行目でbitsteamのデーターを読み込んで、72〜73行目で「general_const.BS_XDEVCFG」ファイルに書き込んでいます。general_const.BS_XDEVCFGの実態は、"/dev/xdevcfg"というデバイスファイルです。どうやらこのデバイスファイルに書き込みを行うことによってOverlay (bitstream) のダウンロードができるようです。

FPGAの制御(FPGAのレジスタへの書き込み)はmmio.pyのMMIOクラスを使って、/dev/memデバイスファイル経由でメモリー空間にマップされたFPGAのレジスタにアクセスしているようです。Pythonのコードは以下になります。 

class MMIO:
    """ This class exposes API for MMIO read and write.

    Attributes
    ----------
    virt_base : int
        The address of the page for the MMIO base address.
    virt_offset : int
        The offset of the MMIO base address from the virt_base.
    base_addr : int
        The base address, not necessarily page aligned.
    length : int
        The length in bytes of the address range.
    debug : bool
        Turn on debug mode if it is True.
    mmap_file : file
        Underlying file object for MMIO mapping
    mem : mmap
        An mmap object created when mapping files to memory.
    array : numpy.ndarray
        A numpy view of the mapped range for efficient assignment

    """

    def __init__(self, base_addr, length=4, debug=False):
        """Return a new MMIO object.

        Parameters
        ----------
        base_addr : int
            The base address of the MMIO.
        length : int
            The length in bytes; default is 4.
        debug : bool
            Turn on debug mode if it is True; default is False.

        """
        if base_addr < 0 or length < 0:
            raise ValueError("Negative offset or negative length.")

        euid = os.geteuid()
        if euid != 0:
            raise EnvironmentError('Root permissions required.')

        # Align the base address with the pages
        self.virt_base = base_addr & ~(mmap.PAGESIZE - 1)

        # Calculate base address offset w.r.t the base address
        self.virt_offset = base_addr - self.virt_base

        # Storing the base address and length
        self.base_addr = base_addr
        self.length = length

        self.debug = debug
        self._debug('MMIO(address, size) = ({0:x}, {1:x} bytes).',
                    self.base_addr, self.length)

        # Open file and mmap
        self.mmap_file = os.open(general_const.MMIO_FILE_NAME,
                                 os.O_RDWR | os.O_SYNC)

        self.mem = mmap.mmap(self.mmap_file, (self.length + self.virt_offset),
                             mmap.MAP_SHARED,
                             mmap.PROT_READ | mmap.PROT_WRITE,
                             offset=self.virt_base)

        self.array = np.frombuffer(self.mem, np.uint32,
                                   length >> 2, self.virt_offset)

    def __del__(self):
        """Destructor to ensure mmap file is closed
        """
        os.close(self.mmap_file)

    def read(self, offset=0, length=4):
        """The method to read data from MMIO.

        Parameters
        ----------
        offset : int
            The read offset from the MMIO base address.
        length : int
            The length of the data in bytes.

        Returns
        -------
        list
            A list of data read out from MMIO

        """
        if not length == 4:
            raise ValueError("MMIO currently only supports 4-byte reads.")
        if offset < 0 or length < 0:
            raise ValueError("Negative offset or negative length.")
        idx = offset >> 2
        if idx << 2 != offset:
            raise MemoryError('Read operation unaligned.')

        self._debug('Reading {0} bytes from offset {1:x}',
                    length, offset)

        # Read data out
        return int(self.array[idx])

    def write(self, offset, data):
        """The method to write data to MMIO.

        Parameters
        ----------
        offset : int
            The write offset from the MMIO base address.
        data : int / bytes
            The integer(s) to be written into MMIO.

        Returns
        -------
        None

        """
        if offset < 0:
            raise ValueError("Negative offset.")

        idx = offset >> 2
        if idx << 2 != offset:
            raise MemoryError('Write operation not aligned.')

        if type(data) is int:
            self._debug('Writing 4 bytes to offset {0:x}: {1:x}',
                        offset, data)
            self.array[idx] = np.uint32(data)
        elif type(data) is bytes:
            length = len(data)
            num_words = length >> 2
            if num_words << 2 != length:
                raise MemoryError('Need an integer number of words')
            buf = np.frombuffer(data, np.uint32, num_words, 0)
            self.array[offset:offset + num_words] = buf
        else:
            raise ValueError("Data type must be int or bytes.")

60行目でopenしている、general_const.MMIO_FILE_NAMEの実態が/dev/memで、63〜66行目でmmapを使ってFPGAのレジスタをマッピングしています。

試験用のbitstreamを作る

試験用に、ZYNQ PSとGPIOのみの最小構成のシステムを作り、GPIOにLED0〜LED3を接続しました。

Image may be NSFW.
Clik here to view.
MyLED Block Design

ZYNQ PLは、Diligent社PYNQサイトのZynq Presetからダウンロードした”pynq_revC.tcl”を使ってPLの設定を行います(Apply Configuration..でtclファイルを指定する)。さらに、ZYNQの構成でMAXI-GP0ポートを有効にします。次にAXI-GPIOを起こして接続したものが上記のブロックデザインになります。

合成・インプリ・ビットストリームの生成を行い、bitstreamをled.bitというファイル名で、PYNQに書き込みます。

動作確認

まずは、標準のOverlayを使ってRGB LEDをLチカするPyhonコードを動かしてみます。Jupyter Notebookは以下の通りです。

Image may be NSFW.
Clik here to view.
Color LED notebook

RGB LEDのLチカが動きました。

次に、一旦動いているNotebookをshutdownします(これを行わずに、Overlayを再ダウンロードするとLinuxが固まる時があります)。

Image may be NSFW.
Clik here to view.
Stop RGB LED

次に、先ほど作ったbitsteamをOverlayとしてダウンロードし、LEDを点滅させるコードを書いてみました。bitstreamクラスやMMIOクラスの低レベル処理をそのまま使っています。

Image may be NSFW.
Clik here to view.
MyLED blink2

Runすると、LED0〜LED3が点滅しました!!

このコードでは、GPIO0のアドレス、0x41200000に対して直接書き込みを行うことによってLEDの点滅を行なっています。非常に低レベルな処理で記述していますが、本来はこれを上位のクラスでラッピングして使いやすいオブジェクトとして見せることになります(今回は、上位クラスの作成はサボっています)。

後書き

今回の実験の成果を3月4日のPYNQ祭りでLTします。Lチカだけではつまらないので、もう少し機能を追加した内容で発表できればと思っています(2月は仕事でドタバタしそうなので、どこまでできるかなのですが)。前段の普通の発表や、@cobac さんのLTと被りそうな気がしますがその際はご容赦を...

PYNQで動的にFPGAをコンフィグして使用することが可能であることが分かりました。これは結構画期的だと思います。Arduinoやmbedの様にオープンソース(ハード?)のOverlayがライブラリとして流通するようになると面白いと思います。

PYNQ-Z1のOverlay読み込みとPythonからのFPGA PLの制御(2)

前回の「PYNQ-Z1のOverlay読み込みとPythonからのFPGA PLの制御」の続編です。前回のポストでは、Overlayのダウンロードに"/dev/xdevcfg"というデバイスファイルを叩いたり、LEDの点滅のためにmmapを直接叩いたりと非常に低レベルな処理になっていました。今回は、PYNQらしく、PythonのOverlayクラスを使ってOverlayをダウンロードし、LEDの制御もMyLEDクラスを使って、より抽象化した処理で動くようにしてみました。

Overlayクラスを使えるようにする

Overlayクラスを使ってOverlayをダウンロードするためには、カスタムOverlayのbitstreamを作った際のVivado tclファイルが必要になります。まず、カスタムOverlayのVivadoプロジェクトを開いて、Block Designを表示します。

Image may be NSFW.
Clik here to view.
MyLED BD

「File > Export > Block Design..」を実行すると、Block Designをtclファイルとしてexportしてくれます。ここでは、led.tclというファイル名で保存しました。

 Image may be NSFW.
Clik here to view.
BD export

生成した、tclファイルをPYNQのカスタムbitstreamと同じディレクトリーに保存します(/home/xilinx/pynq/bitstream)

Image may be NSFW.
Clik here to view.
2017 02 04 Overlay tcl

次に、以下のPyhonコードを実行すると、Overlayクラスのインスタンス作成性時に、led.tclがパースされて、bitstreamに含まれるI/Oとそのアドレスが抽出されます。この例では、axi_gpio_0が0x41200000で識別されていることが分かります。

Image may be NSFW.
Clik here to view.
Instansiate Ovlerlay

次に、axi_gpio_0に値を書き込むことによってLEDを制御するコードを書いて、/home/xilinx/pynq/boardにmyled.pyというファイル名で保存します。14行目のコードで、PL.ip_dictアトリビュートから、GPIOのアドレスを抽出して、mmioクラスのコンストのラクターに渡しています。これで、GPIOのアドレスの紐付けができました。

from pynq import MMIO
from pynq import PL

LEDS_OFFSET0 = 0

class MyLED(object):
    """This class controls the onboard LEDs vi axi_gpio_0. """
    _mmio = None
    _leds_value = 0

    def __init__(self):
        """Create a new MyLED object. """
        if MyLED._mmio is None:
            MyLED._mmio = MMIO(int(PL.ip_dict["SEG_axi_gpio_0_Reg"][0],16),16)
        MyLED._mmio.write(LEDS_OFFSET0, 0x0)

    def set(self, value):
        """Turn on a LED.
        
        Parameters
        ----------
        Value = GPIO out data
        
        Returns
        -------
        None
        
        """
        MyLED._mmio.write(LEDS_OFFSET0, value)

/home/xilinx/pynq/board/__init__.pyに最後の一行を追加しておきます。

from .led import LED
from .rgbled import RGBLED
from .switch import Switch
from .button import Button
from .myled import MyLED

 これでMyLEDクラスが使えるようになりました。

Jupyter Notebookの作成

以下のように、Overlayをダウンロードして、MyLEDクラスを使ってLEDを点灯させます。

Image may be NSFW.
Clik here to view.
Jupyter Notebook

 これで以下のように、カスタムOverlayを使ってLチカができます。前回のコードよりだいぶ簡潔で分かりやすなりました。

後書き

ということで、思ったより簡単にカスタムOverlayをPYNQのお作法に従って動かすことができました。PYNQにもZYBOのように安価なSDSoCのボード限定ライセンスが提供されればソフト屋さんもFPGAを使ったハードウェアオフロードが簡単にできるようになるのではと思います。PYNQのSDSoC、来ないのかな。

 


Viewing all 48 articles
Browse latest View live