【外部映像送受信(4)】Zynq上で外部映像を送受信するアプリケーション(受信系の作成② ソースコードの解説)

Vitis
スポンサーリンク

本記事の概要

概要

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

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

統合ソフトウェア開発環境「Vitis」上で、アプリケーションプロジェクトの作成方法について3回の記事に分けて説明しており、これが最後の記事となります。
前回の記事では受信系の割り込みハンドラの構成について解説しました。

前回の記事で解説した割り込みハンドラの構成に従って、引き続き受信系のアプリケーションプロジェクトを作成します。本記事では外部からのHDMI信号を受信し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メモリに格納(本記事)
④ ①と②を実装し、動作確認

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

ソースコードの例

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

Vitis workspace内の[src]フォルダのファイル構成は次のようにしています。video_inout_receiveradd.cとvideo_inout_receiveradd.hは「Zybo HDMI Input Demo」からダウンロードしたSDKファイルを参考に作成しました。前回の記事で紹介した、video_inout.cに受信系の関数を追加しました。
追加した箇所を中心に説明いたします。

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

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

video_inout_receiveradd.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_bee.h"

#include "xaxivdma.h"
#include "xvtc.h"
#include "xgpio.h"
#include "xscugic.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 VID_VTC_ID          XPAR_VTC_1_DEVICE_ID
#define VID_GPIO_ID         XPAR_AXI_GPIO_VIDEO_DEVICE_ID
#define VID_VTC_IRPT_ID     XPS_FPGA3_INT_ID
#define VID_GPIO_IRPT_ID    XPS_FPGA4_INT_ID
#define INTC_DEVICE_ID      XPAR_SCUGIC_SINGLE_DEVICE_ID

#define DISPLAY_NUM_FRAMES 3

/* ------------------------------------------------------------ */
/*                グローバル変数                                 */
/* ------------------------------------------------------------ */
XAxiVdma    vdma_inst;
XGpio       gpio_inst;
XVtc        vtc_disp;
XVtc        vtc_video;
XScuGic     intc;

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);

/*ビデオ側*/
int VTC_receive_setting(XVtc *vtcptr);
int VDMA_receive_setting(XAxiVdma *vdmaptr, u8 *framePtr[DISPLAY_NUM_FRAMES], u32 stride, XVtc_Timing *timing);
int gpio_setting(XGpio *gpio_inst);
int interrput_controller(void );
void GpioIsr(XGpio *GpioPtr);
void VtcIsr(XVtc *vtcvideoPtr, u32 pendingIrpt);

#endif /* VIDEO_INOUT_H_ */

video_inout_receiveradd.cファイル

#include "video_inout_receiveradd.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++)
        {
            //iPixelAddr = xcoi *3 + ycoi * DEMO_STRIDE;
            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);
    //-----------------------------------------------------
    
    //-----------------------------------------------------
    /* R. 受信側 */
    //-----------------------------------------------------
    /* R.1 GPIOの設定 */
    //-----------------------------------------------------
    // R.1.0 初期化
    Status = XGpio_Initialize(&gpio_inst, VID_GPIO_ID);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }

    // R.1 GPIOの設定
    Status = gpio_setting(&gpio_inst);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }

    //-----------------------------------------------------
    /* R.2 割り込みコントローラの設定 */
    //-----------------------------------------------------
    Status = interrput_controller();
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }
    

    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;
}

//-----------------------------------------------------
/* R.1 GPIOインスタンスの設定 */
//-----------------------------------------------------
int gpio_setting(XGpio *GpioPtr){
    int Status;

    XGpio_SelfTest(GpioPtr);

    // R.1.1 GPIOの入出力設定
    XGpio_DiscreteWrite(GpioPtr, 1, 0); //HPDチャネルからLow状態を出力
    XGpio_SetDataDirection(GpioPtr, 1, 0); //HPDチャネルを出力に設定
    XGpio_SetDataDirection(GpioPtr, 2, 1); //Lockedチャンネルを入力に設定

    // R.1.2 GPIOの割り込みをイネーブル
    XGpio_InterruptEnable(GpioPtr, XGPIO_IR_CH2_MASK);
    XGpio_InterruptGlobalEnable(GpioPtr);

    // R.1.3 HPDチャネルからHigh状態を出力
    XGpio_DiscreteWrite(GpioPtr, 1, 1);
    
    return XST_SUCCESS;
}

//-----------------------------------------------------
/* R.2 割り込みコントローラの設定 */
//-----------------------------------------------------
#define videoGpioIvt(x,y)\
    {x, (XInterruptHandler)GpioIsr, y, 0xA0, 0x3}
#define videoVtcIvt(x,y)\
    {x, (XInterruptHandler)XVtc_IntrHandler, y, 0xB0, 0x3}

typedef struct {
    u8 id;
    XInterruptHandler handler;
    void *pvCallbackRef;
    u8 priority; //not used for microblaze, set to 0
    u8 trigType; //not used for microblaze, set to 0
} ivt_t;

const ivt_t ivt[] = {
    videoGpioIvt(VID_GPIO_IRPT_ID, &gpio_inst),
    videoVtcIvt(VID_VTC_IRPT_ID, &vtc_video)
};

int interrput_controller(void){
    int Status;

    //-----------------------------------------------------
    // R.2.0. 例外処理
    //-----------------------------------------------------
    // R.2.0-1 ARMプロセッサの割り込み発生時に呼び出されるインスタンス(今回はGIC)の割り込みハンドラを登録
    Xil_ExceptionInit();
    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, 
                (Xil_ExceptionHandler)XScuGic_InterruptHandler,
                &intc);
    Xil_ExceptionEnable();

    //-----------------------------------------------------
    // R.2.1. GIC(Generic Interrupt Controller)
    //-----------------------------------------------------
    // R.2.1-1 GICの初期化
    XScuGic_Config *IntcConfig;
    IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
    Status = XScuGic_CfgInitialize(&intc, IntcConfig,
            IntcConfig->CpuBaseAddress);
    if (Status != XST_SUCCESS) {
        return XST_FAILURE;
    }

    // R.2.1-2 GIC割り込み発生時に呼び出されるインスタンスの割り込みハンドラを登録
    unsigned int isIVector;

    for (isIVector = 0; isIVector < sizeof(ivt)/sizeof(ivt[0]); isIVector++)
    {
        XScuGic_SetPriorityTriggerType(&intc, ivt[isIVector].id,
                ivt[isIVector].priority, ivt[isIVector].trigType);

        XScuGic_Connect(&intc, ivt[isIVector].id,
                     (Xil_ExceptionHandler)ivt[isIVector].handler, ivt[isIVector].pvCallbackRef);

        XScuGic_Enable(&intc, ivt[isIVector].id);
    }

    return XST_SUCCESS;
}

//-----------------------------------------------------
/* R.3 GPIOの割り込みハンドラ */
//-----------------------------------------------------
void GpioIsr(XGpio *GpioPtr)
{
    u32 locked;
    int Status;

    // R.3.1 割り込みのペンディングをクリア
    XGpio_InterruptClear(GpioPtr, XGPIO_IR_CH2_MASK);

    // R.3.2 Lockedチャンネルを読み出し
    locked = XGpio_DiscreteRead(GpioPtr, 2);

    if (locked)
    {
        XVtc_Config *vtcConfig;

        // R.3.3 受信側VTCの初期化
        vtcConfig = XVtc_LookupConfig(VID_VTC_ID);
        Status = XVtc_CfgInitialize(&vtc_video, vtcConfig, vtcConfig->BaseAddress);
        if (Status != XST_SUCCESS)
        {
            return XST_FAILURE;
        }

        // R.3.4 受信側VTCのタイミング検出の設定
        Status = VTC_receive_setting(&vtc_video);
        if (Status != XST_SUCCESS)
        {
            return XST_FAILURE;
        }

        // R.3.5 GICをイネーブル
        XScuGic_Enable(&intc, VID_VTC_IRPT_ID);
    }
}

// R.3.4 受信側VTCのタイミング検出の設定
int VTC_receive_setting(XVtc *vtcptr)
{
    XVtc_SelfTest(vtcptr);

    XVtc_RegUpdateEnable(vtcptr);
    XVtc_SetCallBack(vtcptr, XVTC_HANDLER_LOCK, VtcIsr, vtcptr); //VTCのLock信号受信時の割り込みハンドラを設定
    XVtc_IntrEnable(vtcptr, XVTC_IXR_LO_MASK); //VTCのLock信号受信時の割り込みをイネーブル
    XVtc_EnableDetector(vtcptr); //VTCの検出をイネーブル

    return XST_SUCCESS;
}

//-----------------------------------------------------
/* R.4 ビデオ側VTCの割り込みハンドラ */
//-----------------------------------------------------
void VtcIsr(XVtc *vtcvideoPtr, u32 pendingIrpt)
{
    int Status;
    XVtc_Timing vtc_video_timing;

    if ((XVtc_GetDetectionStatus(vtcvideoPtr) & XVTC_STAT_LOCKED_MASK))
    {
        // R.4.1 VTCでHDMI信号のタイミングを検出
        XVtc_GetDetectorTiming(vtcvideoPtr, &vtc_video_timing);

        // R.4.2 受信側VDMAの設定
        Status = VDMA_receive_setting(&vdma_inst, pFrames, DEMO_STRIDE,  &vtc_video_timing);
        if (Status != XST_SUCCESS)
        {
            return XST_FAILURE;
        }


        XVtc_IntrDisable(vtcvideoPtr, XVTC_IXR_LO_MASK);
        XVtc_IntrClear(vtcvideoPtr, XVTC_IXR_LO_MASK);
    }
}

// R.4.2 受信側VDMAの設定
int VDMA_receive_setting(XAxiVdma *vdmaptr, u8 *framePtr[DISPLAY_NUM_FRAMES], u32 stride, XVtc_Timing *timing)
{
    int Status;
    int i;
    XAxiVdma_DmaSetup vdmasetup;
    
    /*---------------------------------------------------------------*/
    /* フレームを読み書きするのに必要な情報を設定 */
    vdmasetup.FrameDelay                = 0;
    vdmasetup.EnableCircularBuf         = 1;
    vdmasetup.EnableSync                = 0;
    vdmasetup.PointNum                  = 0;
    vdmasetup.EnableFrameCounter        = 0;
    vdmasetup.VertSizeInput             = timing->VActiveVideo;
    vdmasetup.HoriSizeInput             = timing->HActiveVideo * 3;
    vdmasetup.FixedFrameStoreAddr       = 0;
    vdmasetup.Stride                    = stride;
    
    for (i = 0; i < DISPLAY_NUM_FRAMES; i++)
    {
        vdmasetup.FrameStoreStartAddr[i] = (u32)  framePtr[i];
    }

    Status = XAxiVdma_StartWriteFrame(vdmaptr, &vdmasetup);
    Status = XAxiVdma_StartParking(vdmaptr, 0, XAXIVDMA_WRITE);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }

    return XST_SUCCESS;
}

ソースコードの解説

では、受信系(ビデオ側)を動作させるためのアプリケーションvideo_inout_receiveradd.cとvideo_inout_receiveradd.hの解説をしていきます。

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

アプリケーションで定義するインスタンスの構成図は次の図の通りです。

まず、アプリケーション上で、各デバイスに対応したインスタンスを定義しました。
前回、送信系で用いるVDMA IPコアとVTC IPコア(ディスプレイ側)のインスタンスをヘッダファイル内で宣言し、main関数内で初期化しました。今回は、さらに追加して、受信系で用いるVTC IPコア(ビデオ側)GPIO IPコア、そして割り込み信号を集中的に管理しCPUに割り込み要求を送信するGIC(Generic Interrupt Controller; 汎用割り込みコントローラ)のインスタンスを追加でヘッダファイル内で宣言しています。

/* ------------------------------------------------------------ */
/*                グローバル変数                                 */
/* ------------------------------------------------------------ */
XAxiVdma    vdma_inst;
XGpio       gpio_inst;
XVtc        vtc_disp;
XVtc        vtc_video;
XScuGic     intc;

main関数のメインルーチン内では、GPIO IPコアGIC(Generic Interrupt Controller; 汎用割り込みコントローラ)のみ、初期化と設定を実行します。

VTC IPコア(ビデオ側)の初期化と設定はまだ行いません。VTC IPコア(ビデオ側)はHDMI信号を受信してから動作を開始するため、HDMI受信時のクロック動作を確立してから発生するGPIOからの割り込み要求を受けたときに初期化・設定することにしています。

#include "video_inout_receiveradd.h"

int main(void)
{
    int Status;
    int i;
    
    //-----------------------------------------------------
    /* S. 送信側*/
    //-----------------------------------------------------
    /* 前回記事「HDMI入出力編(3)」で解説済みのため略 */
    
    //-----------------------------------------------------
    /* R. 受信側 */
    //-----------------------------------------------------
    /* R.1 GPIOの設定 */
    //-----------------------------------------------------
    // R.1.0 初期化
    Status = XGpio_Initialize(&gpio_inst, VID_GPIO_ID);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }

    // R.1 GPIOの設定
    Status = gpio_setting(&gpio_inst);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }

    //-----------------------------------------------------
    /* R.2 割り込みコントローラの設定 */
    //-----------------------------------------------------
    Status = interrput_controller();
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }
    

    while(1){
    };

    return 0;
}

R.1 GPIOの設定

メインルーチンでGPIO IPコアの設定を行います。前回の記事でHDMI動作について解説したときに用いた図を改めて掲載します。

GPIO IPコアにはチャネルを2つ設けます。
チャネル1はHDMI信号のHPD (Hot Plug Detect)チャネルとして外部に出力します。
チャネル2はTMDSチャネルにおけるクロック信号に基づいてクロック動作を確立したときにアサートされる信号をトリガーに、割り込み要求を発生させます。

この動作原理に基づいて、GPIOインスタンスを以下の通り、設定しています。

//-----------------------------------------------------
/* R.1 GPIOインスタンスの設定 */
//-----------------------------------------------------
int gpio_setting(XGpio *GpioPtr){
    int Status;

    XGpio_SelfTest(GpioPtr);

    // R.1.1 GPIOの入出力設定
    XGpio_DiscreteWrite(GpioPtr, 1, 0); //HPDチャネルからLow状態を出力
    XGpio_SetDataDirection(GpioPtr, 1, 0); //HPDチャネルを出力に設定
    XGpio_SetDataDirection(GpioPtr, 2, 1); //Lockedチャンネルを入力に設定

    // R.1.2 GPIOの割り込みをイネーブル
    XGpio_InterruptEnable(GpioPtr, XGPIO_IR_CH2_MASK);
    XGpio_InterruptGlobalEnable(GpioPtr);

    // R.1.3 HPDチャネルからHigh状態を出力
    XGpio_DiscreteWrite(GpioPtr, 1, 1);
    
    return XST_SUCCESS;
}

まず、R.1.1でGPIOの入出力設定について、チャネル1を出力に、チャネル2を入力に設定します。
ちなみに、設定前に、XGpio_DiscreteWrite(GpioPtr, 1, 0)によりHPDチャネルをLow状態に設定しておき、HDMIの接続を切断しておきます。

次に、R.1.2でGPIOのチャネル2の割り込みをイネーブルします。

最後に、R.1.3でチャネル1をHigh状態にしておき、物理的にHDMIケーブルを接続したときにソース機器のHPDチャネルがHigh状態にアサートされるようにしました

R.2 割り込みコントローラの設定

次に、割り込みハンドラ表を設定します。割り込みに関係するAPIの使用方法や概念の解説を、以下の記事にまとめているので、こちらの記事もご覧いただけると嬉しいです。

前回の記事で、割り込みハンドラの構成について整理しました。最終的な結論は、以下の図のようになります。

割り込みハンドラテーブルの分岐
  • 割り込み要求を受信したとき、GICインスタンスの割り込みハンドラテーブルに分岐
    • 割り込み要求の発生源がGPIO IP→GpioIsrという関数を実行し、VTC IP(ビデオ側)の設定とタイミング信号の検出を開始
    • 割り込み要求の発生源がVTC IP(ビデオ側)→vtc_videoインスタンスの割り込みハンドラテーブルに分岐
      • 割り込み要求の要因がLock→VtcIsrという関数を実行し、VDMAの受信設定とDMA動作を開始

この通り構成していきたいのですが、VTC IPコア(ビデオ側)はHDMI信号を受信してから動作を開始するため、GPIOからの割り込み要求を受けたときに初期化・設定することにします。そのため、割り込みハンドラテーブルへのVtcIsrの登録だけは、メインルーチンではなく、「R.3 GPIO割り込み発生時の割り込みハンドラ GpioIsr」上で行います。

ユーザー定義関数interrput_controller()では、この分岐の計画に基づいて、割り込みベクターテーブルGICインスタンスへの割り込みハンドラの登録を行いました。

//-----------------------------------------------------
/* R.2 割り込みコントローラの設定 */
//-----------------------------------------------------
#define videoGpioIvt(x,y)\
    {x, (XInterruptHandler)GpioIsr, y, 0xA0, 0x3}
#define videoVtcIvt(x,y)\
    {x, (XInterruptHandler)XVtc_IntrHandler, y, 0xB0, 0x3}

typedef struct {
    u8 id;
    XInterruptHandler handler;
    void *pvCallbackRef;
    u8 priority; //not used for microblaze, set to 0
    u8 trigType; //not used for microblaze, set to 0
} ivt_t;

const ivt_t ivt[] = {
    videoGpioIvt(VID_GPIO_IRPT_ID, &gpio_inst),
    videoVtcIvt(VID_VTC_IRPT_ID, &vtc_video)
};

int interrput_controller(void){
    int Status;

    //-----------------------------------------------------
    // R.2.0. 例外処理
    //-----------------------------------------------------
    // R.2.0-1 ARMプロセッサの割り込み発生時に呼び出されるインスタンス(今回はGIC)の割り込みハンドラを登録
    Xil_ExceptionInit();
    Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, 
                (Xil_ExceptionHandler)XScuGic_InterruptHandler,
                &intc);
    Xil_ExceptionEnable();

    //-----------------------------------------------------
    // R.2.1. GIC(Generic Interrupt Controller)
    //-----------------------------------------------------
    // R.2.1-1 GICの初期化
    XScuGic_Config *IntcConfig;
    IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
    Status = XScuGic_CfgInitialize(&intc, IntcConfig,
            IntcConfig->CpuBaseAddress);
    if (Status != XST_SUCCESS) {
        return XST_FAILURE;
    }

    // R.2.1-2 GIC割り込み発生時に呼び出されるインスタンスの割り込みハンドラを登録
    unsigned int isIVector;

    for (isIVector = 0; isIVector < sizeof(ivt)/sizeof(ivt[0]); isIVector++)
    {
        XScuGic_SetPriorityTriggerType(&intc, ivt[isIVector].id,
                ivt[isIVector].priority, ivt[isIVector].trigType);

        XScuGic_Connect(&intc, ivt[isIVector].id,
                     (Xil_ExceptionHandler)ivt[isIVector].handler, ivt[isIVector].pvCallbackRef);

        XScuGic_Enable(&intc, ivt[isIVector].id);
    }

    return XST_SUCCESS;
}

まず、R.2.0 例外処理で、割り込みが発生したときにGICインスタンスの割り込みハンドラ表に分岐するようにしています。

次に、R.2.1-1でGICインスタンスを初期化します。
その後、R.2.1-2で、GICインスタンスへ2つの割り込みハンドラを登録しています。このとき、for文を使って、まとめて割り込みハンドラを登録しています。

構造体ivt_tは、割り込みハンドラテーブルにおける1行分をひとかたまりにした変数と考えれば、わかりやすいと思います。つまり、「id」、「割り込みハンドラの関数ポインタ(Handler)」、「割り込みハンドラに送る引数(CallBackRef)」、「優先順位(priority)」、「トリガータイプ(trigType)」をまとめています。

この構造体ivt_tを配列化して、割り込みハンドラテーブルを仮想的に構成するわけです。この自作で構築した仮想的な割り込みハンドラテーブルを、for文を使って機械的にGICの割り込みハンドラテーブルに写しています。

ひがし
ひがし

構造体ivt_tを利用した、割り込みハンドラ表の定義は、Digilent社のデモサンプルを用いました。この構造体はわかりやすいですね。

R.3 GPIO割り込み発生時の割り込みハンドラ GpioIsr

では、HDMI信号を受信しクロック動作が確立したときに実行する、割り込みハンドラGpioIsrの処理を解説します。
この関数でやりたいことは、VTC IP(ビデオ側)の初期化と設定です。

//-----------------------------------------------------
/* R.3 GPIOの割り込みハンドラ */
//-----------------------------------------------------
void GpioIsr(XGpio *GpioPtr)
{
    u32 locked;
    int Status;

    // R.3.1 割り込みのペンディングをクリア
    XGpio_InterruptClear(GpioPtr, XGPIO_IR_CH2_MASK);

    // R.3.2 Lockedチャンネルを読み出し
    locked = XGpio_DiscreteRead(GpioPtr, 2);

    if (locked)
    {
        XVtc_Config *vtcConfig;

        // R.3.3 受信側VTCの初期化
        vtcConfig = XVtc_LookupConfig(VID_VTC_ID);
        Status = XVtc_CfgInitialize(&vtc_video, vtcConfig, vtcConfig->BaseAddress);
        if (Status != XST_SUCCESS)
        {
            return XST_FAILURE;
        }

        // R.3.4 受信側VTCのタイミング検出の設定
        Status = VTC_receive_setting(&vtc_video);
        if (Status != XST_SUCCESS)
        {
            return XST_FAILURE;
        }

        // R.3.5 GICをイネーブル
        XScuGic_Enable(&intc, VID_VTC_IRPT_ID);
    }
}

R.3.2でGPIOのチャネル2を読み出し、確かにHDMI動作がロックされていることを確認できれば、R.3.3で受信側VTCのインスタンスを初期化します。
初期化が完了したら、R.3.4で受信側VTCを設定する関数VTC_receive_setting()を実行します。

// R.3.4 受信側VTCのタイミング検出の設定
int VTC_receive_setting(XVtc *vtcptr)
{
    XVtc_SelfTest(vtcptr);

    XVtc_RegUpdateEnable(vtcptr);
    XVtc_SetCallBack(vtcptr, XVTC_HANDLER_LOCK, VtcIsr, vtcptr); //VTCのLock信号受信時の割り込みハンドラを設定
    XVtc_IntrEnable(vtcptr, XVTC_IXR_LO_MASK); //VTCのLock信号受信時の割り込みをイネーブル
    XVtc_EnableDetector(vtcptr); //VTCの検出をイネーブル

    return XST_SUCCESS;
}

この関数のポイントは2つあります。

まず、関数XVtc_SetCallBack()を用いて、割り込みハンドラにVtcIsrを登録ています。VTCにおけるタイミング信号の検出が完了し、ロック状態になったときにはこの割り込みハンドラが実行されるようになりました。

次に、関数XVtc_IntrEnableを用いて、VTCの割り込みをイネーブルしました。VTCがロック状態になったときに割り込み発生するように、XVTC_IXR_LO_MASKで制限をしています。

このようにして、HDMI信号を確かに受信できてから、VTC(ビデオ側)の設定を開始するようにしています。VTC(ビデオ側)が設定されてから、タイミング検出が完了するまでは、待機状態になります。

ひがし
ひがし

前回説明した状態遷移図において、「VTC検出待機中」という状態になります。

R.4 VTC割り込み発生時の割り込みハンドラ VtcIsr

VTC(ビデオ側)がタイミング信号を検出したときの、割り込み処理を解説しましょう。
このとき、割り込みハンドラVtcIsrが実行されます。
この関数では、検出したタイミング信号をもとに、VDMA IPコアの受信側の設定を行います。

//-----------------------------------------------------
/* R.4 ビデオ側VTCの割り込みハンドラ */
//-----------------------------------------------------
void VtcIsr(XVtc *vtcvideoPtr, u32 pendingIrpt)
{
    int Status;
    XVtc_Timing vtc_video_timing;

    if ((XVtc_GetDetectionStatus(vtcvideoPtr) & XVTC_STAT_LOCKED_MASK))
    {
        // R.4.1 VTCでHDMI信号のタイミングを検出
        XVtc_GetDetectorTiming(vtcvideoPtr, &vtc_video_timing);

        // R.4.2 受信側VDMAの設定
        Status = VDMA_receive_setting(&vdma_inst, pFrames, DEMO_STRIDE,  &vtc_video_timing);
        if (Status != XST_SUCCESS)
        {
            return XST_FAILURE;
        }


        XVtc_IntrDisable(vtcvideoPtr, XVTC_IXR_LO_MASK);
        XVtc_IntrClear(vtcvideoPtr, XVTC_IXR_LO_MASK);
    }
}

GPIO同様に、VTCの状態を確認し、確かにロック状態にあれば、VDMAの設定を開始します。

R.4.1でVTCで検出したタイミング信号を読み出し、それをR.4.2でVDMAの受信側の設定に反映させます。

VDMAの受信側設定は、関数VDMA_receive_settingを用います。

// R.4.2 受信側VDMAの設定
int VDMA_receive_setting(XAxiVdma *vdmaptr, u8 *framePtr[DISPLAY_NUM_FRAMES], u32 stride, XVtc_Timing *timing)
{
    int Status;
    int i;
    XAxiVdma_DmaSetup vdmasetup;
    
    /*---------------------------------------------------------------*/
    /* フレームを読み書きするのに必要な情報を設定 */
    vdmasetup.FrameDelay                = 0;
    vdmasetup.EnableCircularBuf         = 1;
    vdmasetup.EnableSync                = 0;
    vdmasetup.PointNum                  = 0;
    vdmasetup.EnableFrameCounter        = 0;
    vdmasetup.VertSizeInput             = timing->VActiveVideo;
    vdmasetup.HoriSizeInput             = timing->HActiveVideo * 3;
    vdmasetup.FixedFrameStoreAddr       = 0;
    vdmasetup.Stride                    = stride;
    
    for (i = 0; i < DISPLAY_NUM_FRAMES; i++)
    {
        vdmasetup.FrameStoreStartAddr[i] = (u32)  framePtr[i];
    }

    Status = XAxiVdma_StartWriteFrame(vdmaptr, &vdmasetup);
    Status = XAxiVdma_StartParking(vdmaptr, 0, XAXIVDMA_WRITE);
    if (Status != XST_SUCCESS)
    {
        return XST_FAILURE;
    }

    return XST_SUCCESS;
}

基本的には、送信側VDMAの設定と同じように、映像のサイズや書き込むフレームのポインタを設定し、XAxiVdma_StartWriteFrameを用いてフレームへの書き込みを開始します。

ひがし
ひがし

プログラムの説明は以上です。受信側は、割り込みが関係するため、かなり複雑ですね。

動作確認

実装

ダウンロードや作成した一連のファイルを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ポートの接続は不要です。

HDMI入力・VGA出力結果

実装したZybo基板の動作を確認しました。

いくつか気になる点はありますが、なんとかHDMI信号をVGA信号に変換してディスプレイに表示することができました。

気になるところは、主に以下の3つ。
① VTC検出待機中の時間が長い
② ディスプレイの大きさと投影する映像の解像度があっていない
③ 表示にノイズが含まれる(最後のExplorerのシーケンスバーがわかりやすく、本来灰色のシーケンスバーが赤色のモザイク状になっています)

今後、これらは直していければと思います。

ひがし
ひがし

なんとか画像処理を試す準備が整いました!今後は気になる点などを修正しつつ、Zynq上での画像処理に挑戦していきたいと思います。

最後までご覧いただきありがとうございました。

スポンサーリンク


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

開発環境

環境
  • 開発用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評価ボード

コメント