【外部映像送受信(2)】Zynq上で外部映像を送受信するアプリケーション(送信系の作成)

Vitis
スポンサーリンク

本記事の概要

概要

Digilent社では、Zybo用のデモサンプル「Zybo HDMI Input Demo」が提供されています。デモサンプルでは、
(a) DDRメモリから画像を読み出しVGA信号を送信
(b) 外部からのHDMI信号をZynqが受信しDDRメモリに格納
ということを行っています。
本記事では、デモサンプル内のアプリケーションプロジェクトを参考に、(a)の「DDRメモリを読み出しVGA信号を送信」するアプリケーションを作成し、Zyboに実装しました。

以前の記事から、外部から受信した動画像の映像信号をZynqで受信し、それを外部に出力するHDMI入出力デモサンプルを試しています。
以下の記事では、Vivado上でハードウェアデザインを読み込み、ブロックデザインの構成と動作原理を確認しました。

統合ソフトウェア開発環境「Vitis」上で、アプリケーションプロジェクトの作成方法について3回の記事に分けて説明していきます。
まず、本記事では、DDRメモリから静止画を読み出し、VGA信号を出力するソースコードの作成と動作確認までを行うこととしました。

この記事の対象読者
  • FPGAの開発に興味のある学生
  • Vitisを用いたアプリケーション開発は未経験のエンジニア
  • FPGAでの動画像処理を行いたいエンジニア
ひがし
ひがし

それでは、興味のある方はぜひ最後までご覧ください!

目標と工程

開発目標

本記事全体の目標は次の通りです:

統合ソフトウェア開発環境VitisとFPGA設計環境Vivadoを用いて
「HDMI信号を受信しDDRメモリに保存し、それを外部ディスプレイに出力する」
アプリケーションを作成する

開発工程

開発工程

① DigilentのHPからHDMI入力デモのサンプルプログラムをダウンロード
② デモサンプルからZynq SoC内部のハードウェア構成を定めるプロジェクトを読み込む
③ Zynq SoCのARM CPUで動作する”HDMI出力”プログラムを作成

 ③-(a) DDRメモリから画像を読み出しVGA信号を送信(本記事)
 ③-(b) 外部からのHDMI信号をZynqが受信しDDRメモリに格納
④ ①と②を実装し、動作確認

デバイス構成

Zynqの構成

Zynqは外部から映像信号を受信し、PL(Programmable Logic)部からDDRメモリに映像信号を書き込みます。PL内部のIPコアの構成を説明します。HDMI入出力編(1)では、以下の図のようなブロック図の構成に基づき、Vivadoのブロックデザインを作成しました。

構成は大きく、「(a)送信系(ディスプレイ側)」「(b)受信系(ビデオ側)」の2つに分けることができます。

本記事では、「(a)送信系(ディスプレイ側)」を動かすためのアプリケーションを作成していきます。こちらはHDMI出力編で作成した静止画出力アプリケーションと基本的に同じ構成です。VDMA IPコアを通じて、DDRメモリから画像を読み出します。VTC IPコアで生成したタイミング信号と合わせて、ディスプレイへVGA出力します。

HDMI信号の受信はまだ行わず、代わりにDDRメモリに予め格納しておいた静止画をVGA出力しています。次のサンプルビットマップ画像(画素数640×480)をヘッダファイルに変換しておき、DDRメモリ内のフレームバッファに格納しておきました。

ひがし
ひがし

筆者が近くの公園で撮影した画像を使用しました。

我ながらベストショットだと思っています。

過去の記事では、ビットマップ画像をヘッダファイルへ変換するVBAのマクロを紹介しています。

アプリケーションプロジェクトの作成

ソースコードの例

実際に筆者が組んだソースコードを紹介します。

Vitis workspace内の[src]フォルダのファイル構成は次のようにしています。video_inout.cとvideo_inout.hは「Zybo HDMI Input Demo」からダウンロードしたSDKファイルを参考に、作成しました。

“src”フォルダ内のファイルの構成
  • srcフォルダ
    • video_inout.c:本記事で作成したCソースコード
    • video_inout.h:本記事で作成したCヘッダファイル
    • header.h:静止画をヘッダ化したファイル(参考:ビットマップ画像をヘッダファイルに変換するExcel VBAマクロ)
    • vga_modes.h:Digilent社からダウンロードしたファイルをそのまま活用
    • dynclkフォルダ:Digilent社からダウンロードしたファイルをそのまま活用
      • dynclk.c
      • dynclk.h

作成したvideo_inout.cとvideo_inout.hについてソースコードは、以下の通りです。長くなりましたので、[+]マークをクリックしてトグルボックスを開いてご確認いただければと思います。

video_inout.hファイル

#ifndef VIDEO_INOUT_H_
#define VIDEO_INOUT_H_

/* ------------------------------------------------------------ */
/*				Include File Definitions						*/
/* ------------------------------------------------------------ */
#include "xil_types.h"
#include <stdio.h>
#include "xil_cache.h"
#include "xparameters.h"
#include "header.h"

#include "xaxivdma.h"
#include "xvtc.h"

#include "./dynclk/dynclk.h"
#include "vga_modes.h"
/* ------------------------------------------------------------ */
/*					Miscellaneous Declarations					*/
/* ------------------------------------------------------------ */
#define DEMO_MAX_FRAME (1920*1080*3)
#define DEMO_STRIDE (1920 * 3)

#define DYNCLK_BASEADDR     XPAR_AXI_DYNCLK_0_BASEADDR
#define DISP_VDMA_ID        XPAR_AXIVDMA_0_DEVICE_ID
#define DISP_VTC_ID         XPAR_VTC_0_DEVICE_ID

#define DISPLAY_NUM_FRAMES 3

/* ------------------------------------------------------------ */
/*                グローバル変数                                 */
/* ------------------------------------------------------------ */
XAxiVdma    vdma_inst;
XVtc        vtc_disp;

u8 frameBuf[DISPLAY_NUM_FRAMES][DEMO_MAX_FRAME] __attribute__((aligned(0x20)));
u8 *pFrames[DISPLAY_NUM_FRAMES];

/* ------------------------------------------------------------ */
/*                    グローバル関数                             */
/* ------------------------------------------------------------ */
/*ディスプレイ側*/
int dynclk_setting(u32 dynClkAddr, VideoMode vMode);
int VTC_send_setting(XVtc *vtcptr, VideoMode vMode);
int VDMA_send_setting(XAxiVdma *vdma, u8 *framePtr[DISPLAY_NUM_FRAMES], u32 stride, VideoMode vMode);

/************************************************************************/

#endif /* VIDEO_INOUT_H_ */

video_inout.cファイル

#include "video_inout.h"

int main(void)
{
    int Status;
    int i;
    
    //-----------------------------------------------------
    /* S. 送信側*/
    //-----------------------------------------------------
    //-----------------------------------------------------
    /* S.1 dynclk の設定*/
    //-----------------------------------------------------
    Status = dynclk_setting(DYNCLK_BASEADDR, VMODE_640x480);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }

    //-----------------------------------------------------
    /* S.2 VDMAの読み出し側の設定 */
    //-----------------------------------------------------
    // S.2.0 初期化
    XAxiVdma_Config *vdmaConfig;
    vdmaConfig = XAxiVdma_LookupConfig(DISP_VDMA_ID);
    Status = XAxiVdma_CfgInitialize(&vdma_inst, vdmaConfig, vdmaConfig->BaseAddress);

    // S.2 VDMAの読み出し設定
    // 画像を格納するフレームバッファのポインタを取得
    for (i = 0; i < DISPLAY_NUM_FRAMES; i++)
    {
        pFrames[i] = frameBuf[i];
    }

    //設定実行
    Status = VDMA_send_setting(&vdma_inst, pFrames, DEMO_STRIDE, VMODE_640x480);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }

    //-----------------------------------------------------
    /* S.3 送信側VTCの設定 */
    //-----------------------------------------------------
    // S.3.0 初期化
    XVtc_Config *vtcConfig;
    vtcConfig = XVtc_LookupConfig(DISP_VTC_ID);
    Status = XVtc_CfgInitialize(&vtc_disp, vtcConfig, vtcConfig->BaseAddress);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }
    
    // S.3 送信側VTCのタイミング生成の設定
    Status = VTC_send_setting(&vtc_disp, VMODE_640x480);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }

    //-----------------------------------------------------
    /* S.4 フレームバッファへの静止画の入力 */
    //-----------------------------------------------------
    u32 iPixelAddr;
    u32 xcoi, ycoi;
    for(xcoi = 0; xcoi < VMODE_640x480.width; xcoi++)
    {
        iPixelAddr = xcoi * 3;
        for(ycoi = 0; ycoi < VMODE_640x480.height; ycoi++)
        {
            if(xcoi < X_BMP_SIZE && ycoi < Y_BMP_SIZE){
                frameBuf[0][iPixelAddr]       = bmp_file_array[Y_BMP_SIZE - 1 - ycoi][xcoi][1];   //green
                frameBuf[0][iPixelAddr + 1]   = bmp_file_array[Y_BMP_SIZE - 1 - ycoi][xcoi][2];   //blue
                frameBuf[0][iPixelAddr + 2]   = bmp_file_array[Y_BMP_SIZE - 1 - ycoi][xcoi][0];   //red
            }

            iPixelAddr += DEMO_STRIDE;
        }
    }
    Xil_DCacheFlushRange((unsigned int) frameBuf, DEMO_MAX_FRAME);
    //-----------------------------------------------------

    while(1){
    };

    return 0;
}

//-----------------------------------------------------
/* S.1 dynclk の設定*/
//-----------------------------------------------------
int dynclk_setting(u32 dynClkAddr, VideoMode vMode)
{
    ClkConfig clkReg;
    ClkMode clkMode;

    ClkFindParams(vMode.freq, &clkMode);

    if (!ClkFindReg(&clkReg, &clkMode))
    {
        return XST_FAILURE;
    }
    
    ClkWriteReg(&clkReg, dynClkAddr);
    ClkStart(dynClkAddr);

    return XST_SUCCESS;
}

//-----------------------------------------------------
/* S.2 送信側VDMAの設定 */
//-----------------------------------------------------
int VDMA_send_setting(XAxiVdma *vdmaptr, u8 *framePtr[DISPLAY_NUM_FRAMES], u32 stride, VideoMode vMode)
{
    int i;
    int Status;
    XAxiVdma_DmaSetup   vdmasetup;

    // S.2.1 フレームを読み書きするのに必要な情報を設定
    vdmasetup.FrameDelay                = 0;
    vdmasetup.EnableCircularBuf         = 1;
    vdmasetup.EnableSync                = 0;
    vdmasetup.PointNum                  = 0;
    vdmasetup.EnableFrameCounter        = 0;
    vdmasetup.VertSizeInput             = vMode.height;
    vdmasetup.HoriSizeInput             = (vMode.width) * 3;
    vdmasetup.FixedFrameStoreAddr       = 0;
    vdmasetup.Stride                    = stride;
    for (i = 0; i < DISPLAY_NUM_FRAMES; i++)
    {
        vdmasetup.FrameStoreStartAddr[i]  = (u32)  framePtr[i];
    }

    // S.2.2 VideoDMA読み出し開始
    Status = XAxiVdma_StartReadFrame(vdmaptr, &vdmasetup);

    // S.2.3 送受信するフレームを特定のフレーム(0)に固定
    Status = XAxiVdma_StartParking(vdmaptr, 0, XAXIVDMA_READ);

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

//-----------------------------------------------------
/* S.3 送信側VTCの設定 */
//-----------------------------------------------------
int VTC_send_setting(XVtc *vtcptr, VideoMode vMode)
{
    XVtc_Timing vtcTiming;
    XVtc_SourceSelect SourceSelect;
    
    // S.3.1 タイミングを制御するのに必要な情報を設定
    vtcTiming.HActiveVideo  = vMode.width                     ;    /**< Horizontal Active Video Size */
    vtcTiming.HFrontPorch   = vMode.hps    - vMode.width      ;    /**< Horizontal Front Porch Size */
    vtcTiming.HSyncWidth    = vMode.hpe    - vMode.hps        ;    /**< Horizontal Sync Width */
    vtcTiming.HBackPorch    = vMode.hmax   - vMode.hpe + 1    ;    /**< Horizontal Back Porch Size */
    vtcTiming.HSyncPolarity = vMode.hpol                      ;    /**< Horizontal Sync Polarity */
    vtcTiming.VActiveVideo  = vMode.height                    ;    /**< Vertical Active Video Size */
    vtcTiming.V0FrontPorch  = vMode.vps    - vMode.height     ;    /**< Vertical Front Porch Size */
    vtcTiming.V0SyncWidth   = vMode.vpe    - vMode.vps        ;    /**< Vertical Sync Width */
    vtcTiming.V0BackPorch   = vMode.vmax   - vMode.vpe + 1    ;    /**< Horizontal Back Porch Size */
    vtcTiming.V1FrontPorch  = vMode.vps    - vMode.height     ;    /**< Vertical Front Porch Size */
    vtcTiming.V1SyncWidth   = vMode.vpe    - vMode.vps        ;    /**< Vertical Sync Width */
    vtcTiming.V1BackPorch   = vMode.vmax   - vMode.vpe + 1    ;    /**< Horizontal Back Porch Size */
    vtcTiming.VSyncPolarity = vMode.vpol                      ;    /**< Vertical Sync Polarity */
    vtcTiming.Interlaced    = 0                               ;    /**< Interlaced / Progressive video */

    memset((void *)&SourceSelect, 0, sizeof(SourceSelect));
    SourceSelect.VBlankPolSrc       = 1;
    SourceSelect.VSyncPolSrc        = 1;
    SourceSelect.HBlankPolSrc       = 1;
    SourceSelect.HSyncPolSrc        = 1;
    SourceSelect.ActiveVideoPolSrc  = 1;
    SourceSelect.ActiveChromaPolSrc = 1;
    SourceSelect.VChromaSrc         = 1;
    SourceSelect.VActiveSrc         = 1;
    SourceSelect.VBackPorchSrc      = 1;
    SourceSelect.VSyncSrc           = 1;
    SourceSelect.VFrontPorchSrc     = 1;
    SourceSelect.VTotalSrc          = 1;
    SourceSelect.HActiveSrc         = 1;
    SourceSelect.HBackPorchSrc      = 1;
    SourceSelect.HSyncSrc           = 1;
    SourceSelect.HFrontPorchSrc     = 1;
    SourceSelect.HTotalSrc          = 1;

    // S.3.2 VTCトランザクションの開始
    XVtc_SelfTest(vtcptr);
    XVtc_RegUpdateEnable(vtcptr);
    XVtc_SetGeneratorTiming(vtcptr, &vtcTiming);
    XVtc_SetSource(vtcptr, &SourceSelect);
    XVtc_EnableGenerator(vtcptr);

    return XST_SUCCESS;
}

ソースコードの解説

では、送信系(ディスプレイ側)を動作させるためのアプリケーションvideo_inout.cとvideo_inout.hの解説をしていきます。

インスタンスの宣言と初期化

アプリケーションで定義するインスタンスの構成図は次の図の通りです。
GICインスタンスやGPIOインスタンス、vtc_videoインスタンスは受信系(ビデオ側)で宣言、初期化しています。次回の記事で解説します。

まず、アプリケーション上で、各デバイスに対応したインスタンスを定義していきます。
送信系で用いられるデバイスのうち、CPU上で各種設定が必要なIPコアは次の3つです。Xilinx社が提供するVDMA IPコアとVTC IPコア、それに加えてDigilentが提供するダイナミッククロックを動かすIPコアです。
Xilinx SDKやVitisのライブラリには、VDMA IPコアとVTC IPコアに対応するインスタンスXAXIVdmaとXVtcが用意されているので、各インスタンスをヘッダファイル内で宣言し、main関数内で初期化しています。

/* ------------------------------------------------------------ */
/*                グローバル変数                                 */
/* ------------------------------------------------------------ */
XAxiVdma    vdma_inst;
XVtc        vtc_disp;
int main(void)
{
    /*略*/

    //-----------------------------------------------------
    /* S.1 dynclk の設定*/
    //-----------------------------------------------------
    /*略*/

    //-----------------------------------------------------
    /* S.2 VDMAの読み出し側の設定 */
    //-----------------------------------------------------
    // S.2.0 初期化
    XAxiVdma_Config *vdmaConfig;
    vdmaConfig = XAxiVdma_LookupConfig(DISP_VDMA_ID);
    Status = XAxiVdma_CfgInitialize(&vdma_inst, vdmaConfig, vdmaConfig->BaseAddress);

    // S.2 VDMAの読み出し設定
    /*略*/

    //-----------------------------------------------------
    /* S.3 送信側VTCの設定 */
    //-----------------------------------------------------
    // S.3.0 初期化
    XVtc_Config *vtcConfig;
    vtcConfig = XVtc_LookupConfig(DISP_VTC_ID);
    Status = XVtc_CfgInitialize(&vtc_disp, vtcConfig, vtcConfig->BaseAddress);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }
    
    // S.3 送信側VTCのタイミング生成の設定
    /*略*/

    return 0;
}

S.1 ダイナミッククロックの設定

Digilentが提供するダイナミッククロックを動かすIPコアのインスタンスは用意されていないようでしたので、ダイナミッククロックのIPコアの制御レジスタを直接叩いています。

ダイナミッククロックの制御レジスタの設定は、関数dynclk_settingで行っています。
設定したビデオモードに対応できるように、自動でクロック周波数を計算しています。また、その計算した周波数になるように、ダイナミッククロックIPコアのレジスタを設定しています。

ひがし
ひがし

Digilentがすべてドライバ関数を提供しているので、ダイナミッククロックの設定はほとんど苦労せずに行うことができます。

dynclk_settingは設定に必要な一連の関数をまとめています。

S.2 VDMAの読み出し側の設定

次に、VDMA IPコアの読み出しの設定です。VDMAはメモリへの書き込みも行いますが、書き込みは次回の記事で解説します。

VDMA IPコアのドライバの使用方法の詳細は、以下の記事をご覧ください。

静止画を格納したフレームバッファの指定
u8 frameBuf[DISPLAY_NUM_FRAMES][DEMO_MAX_FRAME] __attribute__((aligned(0x20)));
u8 *pFrames[DISPLAY_NUM_FRAMES];
int main(void)
{
    /*略*/
    // S.2 VDMAの読み出し設定
    // 画像を格納するフレームバッファのポインタを取得
    for (i = 0; i < DISPLAY_NUM_FRAMES; i++)
    {
        pFrames[i] = frameBuf[i];
    }

    //設定実行
    Status = VDMA_send_setting(&vdma_inst, pFrames, DEMO_STRIDE, VMODE_640x480);
}

画像を格納するフレームバッファframeBufを宣言しておき、frameBufのポインタをpFramesとして定義しています。
VDMAではこのフレームバッファを読み出すように設定します。

S.2.1 フレームを読み書きするのに必要な情報を設定
//-----------------------------------------------------
/* S.2 送信側VDMAの設定 */
//-----------------------------------------------------
int VDMA_send_setting(XAxiVdma *vdmaptr, u8 *framePtr[DISPLAY_NUM_FRAMES], u32 stride, VideoMode vMode)
{
    int i;
    int Status;
    XAxiVdma_DmaSetup   vdmasetup;

    // S.2.1 フレームを読み書きするのに必要な情報を設定
    vdmasetup.FrameDelay                = 0;
    vdmasetup.EnableCircularBuf         = 1;
    vdmasetup.EnableSync                = 0;
    vdmasetup.PointNum                  = 0;
    vdmasetup.EnableFrameCounter        = 0;
    vdmasetup.VertSizeInput             = vMode.height;
    vdmasetup.HoriSizeInput             = (vMode.width) * 3;
    vdmasetup.FixedFrameStoreAddr       = 0;
    vdmasetup.Stride                    = stride;
    for (i = 0; i < DISPLAY_NUM_FRAMES; i++)
    {
        vdmasetup.FrameStoreStartAddr[i]  = (u32)  framePtr[i];
    }

    // S.2.2 VideoDMA読み出し開始
    /*略*/
    // S.2.3 送受信するフレームを特定のフレーム(0)に固定
    /*略*/

    return  XST_SUCCESS;
}

構造体vdmasetupにVDMAの設定を格納していきます。
指定したビデオモード、今回であれば640×480のモードになるように、入力サイズなどを指定していきます。
また、フレームバッファのポインタpFrameも構造体vdmasetupに格納します。

S.2.2 VideoDMA読み出し開始 / S.2.3 送受信するフレームを特定のフレーム(0)に固定
//-----------------------------------------------------
/* S.2 送信側VDMAの設定 */
//-----------------------------------------------------
int VDMA_send_setting(XAxiVdma *vdmaptr, u8 *framePtr[DISPLAY_NUM_FRAMES], u32 stride, VideoMode vMode)
{
    // S.2.1 フレームを読み書きするのに必要な情報を設定
    /*略*/

    // S.2.2 VideoDMA読み出し開始
    Status = XAxiVdma_StartReadFrame(vdmaptr, &vdmasetup);

    // S.2.3 送受信するフレームを特定のフレーム(0)に固定
    Status = XAxiVdma_StartParking(vdmaptr, 0, XAXIVDMA_READ);
    return  XST_SUCCESS;
}

ドライバ関数XAxiVdma_StartReadFrameを使って、一気に読み出し設定を行っています。

最後に、送信するフレームを0に固定し、常に特定の同じフレームを参照するようにしました。

ひがし
ひがし

以上で、VDMAの読み出し設定は完了です。VDMAの読み出し設定はドライバ関数XAxiVdma_StartReadFrameを使って一気に行うのが楽だと思います。

S.3 ディスプレイ側のタイミング生成に用いるVTCの設定

最後に、ディスプレイ側のタイミング生成に用いるVTC IPコアの設定です。ビデオ側のタイミング検出に用いるVTC IPコアについては次回の記事で解説します。

VTC IPコアのドライバの使用方法の詳細は、以下の記事をご覧ください。

S.3.1 タイミングを制御するのに必要な情報を設定
//-----------------------------------------------------
/* S.3 送信側VTCの設定 */
//-----------------------------------------------------
int VTC_send_setting(XVtc *vtcptr, VideoMode vMode)
{
    XVtc_Timing vtcTiming;
    XVtc_SourceSelect SourceSelect;
    
    // S.3.1 タイミングを制御するのに必要な情報を設定
    vtcTiming.HActiveVideo  = vMode.width                     ;    /**< Horizontal Active Video Size */
    vtcTiming.HFrontPorch   = vMode.hps    - vMode.width      ;    /**< Horizontal Front Porch Size */
    vtcTiming.HSyncWidth    = vMode.hpe    - vMode.hps        ;    /**< Horizontal Sync Width */
    vtcTiming.HBackPorch    = vMode.hmax   - vMode.hpe + 1    ;    /**< Horizontal Back Porch Size */
    vtcTiming.HSyncPolarity = vMode.hpol                      ;    /**< Horizontal Sync Polarity */
    vtcTiming.VActiveVideo  = vMode.height                    ;    /**< Vertical Active Video Size */
    vtcTiming.V0FrontPorch  = vMode.vps    - vMode.height     ;    /**< Vertical Front Porch Size */
    vtcTiming.V0SyncWidth   = vMode.vpe    - vMode.vps        ;    /**< Vertical Sync Width */
    vtcTiming.V0BackPorch   = vMode.vmax   - vMode.vpe + 1    ;    /**< Horizontal Back Porch Size */
    vtcTiming.V1FrontPorch  = vMode.vps    - vMode.height     ;    /**< Vertical Front Porch Size */
    vtcTiming.V1SyncWidth   = vMode.vpe    - vMode.vps        ;    /**< Vertical Sync Width */
    vtcTiming.V1BackPorch   = vMode.vmax   - vMode.vpe + 1    ;    /**< Horizontal Back Porch Size */
    vtcTiming.VSyncPolarity = vMode.vpol                      ;    /**< Vertical Sync Polarity */
    vtcTiming.Interlaced    = 0                               ;    /**< Interlaced / Progressive video */

    memset((void *)&SourceSelect, 0, sizeof(SourceSelect));
    SourceSelect.VBlankPolSrc       = 1;
    SourceSelect.VSyncPolSrc        = 1;
    SourceSelect.HBlankPolSrc       = 1;
    SourceSelect.HSyncPolSrc        = 1;
    SourceSelect.ActiveVideoPolSrc  = 1;
    SourceSelect.ActiveChromaPolSrc = 1;
    SourceSelect.VChromaSrc         = 1;
    SourceSelect.VActiveSrc         = 1;
    SourceSelect.VBackPorchSrc      = 1;
    SourceSelect.VSyncSrc           = 1;
    SourceSelect.VFrontPorchSrc     = 1;
    SourceSelect.VTotalSrc          = 1;
    SourceSelect.HActiveSrc         = 1;
    SourceSelect.HBackPorchSrc      = 1;
    SourceSelect.HSyncSrc           = 1;
    SourceSelect.HFrontPorchSrc     = 1;
    SourceSelect.HTotalSrc          = 1;

    /*略*/
}

ビデオ信号のタイミングを設定しています。
vtcTimingという構造体に、ブランキング期間や同期期間などのタイミングに関する情報を格納しています。
次に、SourceSelectという構造体に、検出系で取得したタイミング信号を用いるのか(0)、外部から入力されたタイミング信号を用いるのか(1)という情報を格納しています。

SourceSelectとは

タイミング信号を生成する際に、どのレジスタを参照するかを選択するためのレジスタ。

  • 0を指定:Detector Registerを参照 (検出したビデオフォーマットを格納するレジスタ)
  • 1を指定:Generator Registerを参照 (外部から入力された指定のビデオフォーマットを格納する生成用のレジスタ)
ひがし
ひがし

今回の例では、外部入力でビデオフォーマットを指定していくので、全て1に設定しています。
今回はやりませんが、外部のHDMI入力信号から検出したタイミング信号をもとに、出力のタイミング信号を決めても面白そうですね。

S.3.2 VTCトランザクションの開始
//-----------------------------------------------------
/* S.3 送信側VTCの設定 */
//-----------------------------------------------------
int VTC_send_setting(XVtc *vtcptr, VideoMode vMode)
{
    /*略*/

    // S.3.2 VTCトランザクションの開始
    XVtc_SelfTest(vtcptr);

    XVtc_RegUpdateEnable(vtcptr);
    XVtc_SetGeneratorTiming(vtcptr, &vtcTiming);
    XVtc_SetSource(vtcptr, &SourceSelect);

    XVtc_EnableGenerator(vtcptr);

    return XST_SUCCESS;
}

構造体vtcTimingとSourceSelectに基づいて、VTCコアのレジスタに設定を書き込んでいきます。
まず、関数XVtc_RegUpdateEnableによってレジスタの更新を実行できるようにします。その後、関数XVtc_SetGeneratorTimingによって、構造体vtcTimingを生成レジスタに書き込みます。次に、関数XVtc_SetSourceによって、構造体SourceSelectを制御レジスタに値を書き込みます。最後に、関数XVtc_EnableGeneratorによってVTCコアを実行します。

ひがし
ひがし

以上で、ディスプレイ側のVTCの設定は完了です。

S.4 フレームバッファへの静止画の入力

#include "video_inout.h"

int main(void)
{
    /*略*/

    //-----------------------------------------------------
    /* S.4 フレームバッファへの静止画の入力 */
    //-----------------------------------------------------
    u32 iPixelAddr;
    u32 xcoi, ycoi;
    for(xcoi = 0; xcoi < VMODE_640x480.width; xcoi++)
    {
        iPixelAddr = xcoi * 3;
        for(ycoi = 0; ycoi < VMODE_640x480.height; ycoi++)
        {
            if(xcoi < X_BMP_SIZE && ycoi < Y_BMP_SIZE){
                frameBuf[0][iPixelAddr]       = bmp_file_array[Y_BMP_SIZE - 1 - ycoi][xcoi][1];   //green
                frameBuf[0][iPixelAddr + 1]   = bmp_file_array[Y_BMP_SIZE - 1 - ycoi][xcoi][2];   //blue
                frameBuf[0][iPixelAddr + 2]   = bmp_file_array[Y_BMP_SIZE - 1 - ycoi][xcoi][0];   //red
            }

            iPixelAddr += DEMO_STRIDE;
        }
    }
    Xil_DCacheFlushRange((unsigned int) frameBuf, DEMO_MAX_FRAME);
    //-----------------------------------------------------

    while(1){
    };

    return 0;
}

配列frameBufに静止画の画素値を書き込んでいます。
HDMI信号を受信した場合は、受信した画像をこのframeBufに入力することになります。

最後に、Xil_DCacheFlushRangeでキャッシュコヒーレンシを保つようにしました。
キャッシュコヒーレンシに関する解説や、保たないとどうなるかについては、以下の記事で解説しています。

動作確認

実装

ダウンロードや作成した一連のファイルをworkspaceのsrcフォルダに移動させたらビルドし、実装まで行いました。
実装までの手順の詳細を改めて[+]マーク以下に記載しておきます。

①プロジェクトのビルド
エクスプローラ内のプラットフォームプロジェクト(hdmi_out_system_wrapper)を選択し、右クリックします。[Build Project]を選択すると、ビルドが始まります。

次に、エクスプローラ内のシステムプロジェクト(hdmi_out_application_system)を選択し、右クリックします。同様に[Build Project]を選択すると、ビルドが始まります。

②コンフィグレーション
回路基板をホストPCにつなぎます。
つないで電源を入れたら、PC側からFPGAへのbitstreamファイルの書き込み(コンフィグレーション)とアプリケーションソフトウェアのDDRへの書き込みを実行します。

“hdmi_out_application_system”を右クリックし、[Debug as]>[Launch Hardware]を選択すると、コンフィグレーションとソフトウェアの書き込みが実施されます。
※今回の記事ではCOMポートの接続は不要です。

VGA出力結果

Zybo上に作成した一連のアプリケーションプロジェクトを実装して、VGA端子からディスプレイに画像を出力してみました。

問題なく、サンプル画像がVGA出力できることを確認しました。

ひがし
ひがし

次回はHDMI信号の受信側のプログラムを解説します。このプログラムでは、HDMI端子にコードを差したときに応答できるように割り込みコントローラを使っています。そのため、少し複雑な構成になるので、別記事にて解説したいと思います。
最後まで、ご覧いただきありがとうございました!

次回の記事へのリンク

参考:コードの動作を確認したシステムの構成

開発環境

環境
  • 開発用PC: Windows 10, 64bit
    • Vivado Design Suite – HLx Edition – 2020.2
    • Vitis コア開発キット – 2020.2
  • 開発用基板: Zybo Zynq-7010評価ボード(Board Rev.4)
    • Zynq XC7Z010-1CLG400C

開発ボード Zybo Zynq-7010評価ボード

コメント