[i.MXRT1060]emWinでテトリスを作ってみた〜第2回〜


さて、今回から実際に画面にテトリスのコードを実装して行きたいと思います。そして、テトリミノ(ブロック)が表示されるか確認したいと思います。

第2回でやること

  1. テトリミノの表示
  2. プロジェクトにビットマップのバイナリーを取り込む
  3. テトリスのコード実装(一部)

今回は、これだけです。

尚、2)のプロジェクトにビットマップバイナリーを取り込んで画面に描画することには、結局失敗しています。参考程度で見て下さい。

emWinについて
emWinのライブラリはNXPのマイコンではフリーで使用出来ますが、GUI画面をデザインするGUI biulderなどのツールは無償では使用出来ません。また、各種コンバーターツールなど便利なツールがSegger社からリリースされています。

テトリミノの表示

早速、テトリミノの表示から確認していきます。

前回、テトリミノのイメージを自作しました。このイメージはBMP(ビットマップ)形式で、Cortex-M用にBMPバイナリーを変換しました。

この変換ファイルをMCUXPresso IDEに取り込んで、そのファイルにアクセスして表示してみます。

emWinでテトリスのブロックを表示できれば、以前実装したテトリスのコードがあるので、一気に出来上がりに近くなります。

やりたいことは、一歩一歩確認していくのが早道です。

MCUXPresso IDEへの取り込み設定

まず、前回変換したファイルをMCUXpresso IDEに取り込むことから始まります。

バイナリーファイルの取り込み方法は、以前紹介した記事も参考にしてください。

以前の記事はこちらから

まず、変換したバイナリファイルをIDEに取り込みます。

プロジェクトにフォルダを作成する
プロジェクトにフォルダを作成する

作成したフォルダーにバイナリー変換したファイルをドラッグ&ドロップしてコピーを選びます。

各バイナリーファイルをドラッグ&ドロップ
各バイナリーファイルをドラッグ&ドロップ
プロジェクトにコピーする
プロジェクトにコピーする
プロジェクトに取り込んだところ
プロジェクトに取り込んだところ

これで、プロジェクトへの取り込みは完了です。

MCUXPresso IDEのプロパティ設定

バイナリーをプロジェクトに取り込んだら、プロパティで取り込んだファイルを指定してあげる必要があります。

Settings-Tool Settingタブ-MCU Linker-MIscellaneousに追加したファイルを指定します。

これでバイナリーファイルの指定は完了です。あとは、Cソースの中で各バイナリーコードの先頭アドレス(配列として定義)にアクセスするだけで簡単にアクセス出来ます。

BMPイメージへのコード実装

取り込んだBMPのバイナリーコードへのアクセスは、次のように定義するだけです。

まずは、灰色のブロックだけを試しに定義してブロックが表示できるか確認していきます。

/*******************************************************************************
 * Variables
 ******************************************************************************/
extern char _binary_gray_20_bmp_start[];
extern char _binary_gray_20_bmp_end[];
extern char _binary_gray_20_bmp_size[];
const char *gray = _binary_gray_20_bmp_start;

テトリスのコード実装

テトリスのコードは、以前LPC54104でコンソールのアスキーアートでテトリスを実装してみたコードを使用します。

以前の記事はこちらから→

まずは、下記のコードだけをコピペしてきて、ビルドできるか確認していきます。evkmimxrt1060_emwin_serial_terminalのプロジェクトに下記コードを追加します。

#define FIELD_WIDTH 12
#define FIELD_HEIGHT 22

char field[FIELD_HEIGHT][FIELD_WIDTH];
char dispBuffer[FIELD_HEIGHT][FIELD_WIDTH]={0};
enum{
	MINO_TYPE_I,
	MINO_TYPE_O,
	MINO_TYPE_S,
	MINO_TYPE_Z,
	MINO_TYPE_J,
	MINO_TYPE_L,
	MINO_TYPE_T,
	MINO_TYPE_MAX
};

enum{
	MINO_ANGLE_0,
	MINO_ANGLE_90,
	MINO_ANGLE_180,
	MINO_ANGLE_270,
	MINO_ANGLE_MAX
};
#define MINO_WIDTH 4
#define MINO_HEIGHT 4

char minoShapes[MINO_TYPE_MAX][MINO_ANGLE_MAX][MINO_WIDTH][MINO_HEIGHT] = {
		{//MINO_TYPE_I,
			//MINO_ANGLE_0,
			{
					{0, 1, 0, 0},
					{0, 1, 0, 0},
					{0, 1, 0, 0},
					{0, 1, 0, 0},
			},
				//MINO_ANGLE_90,
			{
					{0, 0, 0, 0},
					{0, 0, 0, 0},
					{1, 1, 1, 1},
					{0, 0, 0, 0},
			},
				//MINO_ANGLE_180,
			{
					{0, 0, 1 ,0},
					{0, 0, 1, 0},
					{0, 0, 1, 0},
					{0, 0, 1, 0},
			},
				//MINO_ANGLE_270,
			{
					{0, 0 ,0 ,0},
					{1, 1 ,1 ,1},
					{0, 0 ,0 ,0},
					{0, 0 ,0 ,0},
			},
		},
		//MINO_TYPE_O,
		{
			//MINO_ANGLE_0,
			{
					{0, 0, 0 ,0},
					{0, 1, 1 ,0},
					{0, 1, 1 ,0},
					{0, 0, 0 ,0},
			},
			//MINO_ANGLE_90,
			{
					{0, 0, 0, 0},
					{0, 1, 1, 0},
					{0, 1, 1, 0},
					{0, 0, 0 ,0},
			},
			//MINO_ANGLE_180,
			{
					{0, 0, 0, 0},
					{0, 1, 1, 0},
					{0, 1, 1, 0},
					{0, 0, 0, 0},
			},
			//MINO_ANGLE_270,
			{
					{0, 0, 0, 0},
					{0, 1, 1, 0},
					{0, 1 ,1, 0},
					{0, 0, 0, 0},
			},
		},
		//MINO_TYPE_S,
		{
			//MINO_ANGLE_0,
			{
					{0, 0, 0, 0},
					{0, 1, 1, 0},
					{1, 1, 0, 0},
					{0, 0, 0, 0},
			},
			//MINO_ANGLE_90,
			{
					{0, 1, 0, 0},
					{0, 1, 1, 0},
					{0, 0, 1, 0},
					{0, 0, 0, 0},
			},
			//MINO_ANGLE_180,
			{
					{0, 0, 0 ,0},
					{0, 0, 1, 1},
					{0, 1, 1 ,0},
					{0, 0, 0 ,0},
			},
			//MINO_ANGLE_270,
			{
					{0, 0, 0, 0},
					{0, 1, 0, 0},
					{0, 1, 1, 0},
					{0, 0, 1, 0},
			},
		},
		//MINO_TYPE_Z,
		{
			//MINO_ANGLE_0,
			{
					{0, 0, 0, 0},
					{1, 1, 0, 0},
					{0, 1, 1, 0},
					{0, 0, 0, 0},
			},
			//MINO_ANGLE_90,
			{
					{0, 0, 0, 0},
					{0, 0, 1, 0},
					{0, 1, 1, 0},
					{0, 1, 0, 0},
			},
			//MINO_ANGLE_180,
			{
					{0, 0, 0, 0},
					{0, 1, 1, 0},
					{0, 0, 1, 1},
					{0, 0, 0, 0},
			},
			//MINO_ANGLE_270,
			{
					{0, 0, 1, 0},
					{0, 1, 1, 0},
					{0, 1, 0, 0},
					{0, 0, 0, 0},
			},
		},
		//MINO_TYPE_J,
		{
			//MINO_ANGLE_0,
			{
					{0, 0, 1, 0},
					{0, 0, 1, 0},
					{0, 1, 1, 0},
					{0, 0, 0, 0},
			},
			//MINO_ANGLE_90,
			{
					{0, 0, 0, 0},
					{1, 1, 1, 0},
					{0, 0, 1, 0},
					{0, 0, 0, 0},
			},
			//MINO_ANGLE_180,
			{
					{0, 0, 0, 0},
					{0, 1, 1, 0},
					{0, 1, 0, 0},
					{0, 1, 0, 0},
			},
			//MINO_ANGLE_270,
			{
					{0, 0, 0, 0},
					{0, 1, 0, 0},
					{0, 1, 1, 1},
					{0, 0, 0, 0},
			},
		},
		//MINO_TYPE_L,
		{
			//MINO_ANGLE_0,
			{
					{0, 1, 0, 0},
					{0, 1, 0, 0},
					{0, 1, 1, 0},
					{0, 0, 0, 0},
			},
			//MINO_ANGLE_90,
			{
					{0, 0, 0, 0},
				    {0, 0, 1, 0},
					{1, 1, 1, 0},
					{0, 0, 0, 0},
	        },
			//MINO_ANGLE_180,
			{
					{0, 0, 0, 0},
					{0, 1, 1, 0},
					{0, 0, 1, 0},
					{0, 0, 1, 0},
		    },
			//MINO_ANGLE_270,
			{
					{0, 0, 0, 0},
					{0, 1, 1, 1},
					{0, 1, 0, 0},
					{0, 0, 0, 0},
            },
		},
		//MINO_TYPE_T,
		{
			//MINO_ANGLE_0,
			{
					{0, 0, 0, 0},
					{1, 1, 1, 0},
					{0, 1, 0, 0},
					{0, 0, 0, 0},
			},
			//MINO_ANGLE_90,
			{
					{0, 0, 0, 0},
					{0, 1, 0, 0},
					{0, 1, 1, 0},
					{0, 1, 0, 0},
			},
			//MINO_ANGLE_180,
			{
					{0, 0, 0, 0},
					{0, 0, 1, 0},
					{0, 1, 1, 1},
					{0, 0, 0, 0},
			},
			//MINO_ANGLE_270,
			{
					{0, 0, 1, 0},
					{0, 1, 1, 0},
					{0, 0, 1, 0},
					{0, 0, 0, 0},
			},
		},

};


int minoX=5, minoY=1;
uint32_t minoType=0, minoAngle=0;

void display(){
	utickIntFlag = false;
	/* Screen clear  */
	PRINTF("\033[2J");   	//Escape Sequence Clear screen
	//system("cls");
	PRINTF("\033[1;1f");	//Cursor moves to (1,1)
	memcpy(dispBuffer, field, sizeof(field));
	for (int y =0; y< MINO_HEIGHT; y++)
		for(int x =0; x < MINO_WIDTH; x++)
			dispBuffer[minoY +y][minoX+x] |= minoShapes[minoType][minoAngle][y][x];

	/* Drawing */
	for (int y =0; y < FIELD_HEIGHT; y++){
		for(int x=0; x<FIELD_WIDTH; x++){
			
		    if (dispBuffer[y][x]){
			PRINTF("◽");
		    }else{
			PRINTF("  ");
		    }

		}
	    PRINTF("\n\r");
	}

}

 

int main(void)
{
    /* Board pin, clock, debug console init */
    BOARD_ConfigMPU();
    BOARD_InitPins();
    BOARD_InitI2C1Pins();
    BOARD_InitSemcPins();
    BOARD_BootClockRUN();
    BOARD_InitLcdifPixelClock();
    BOARD_InitDebugConsole();
    BOARD_InitLcd();

    /* emWin start */
    GUI_Init();

    WM_SetSize(WM_HBKWIN, LCD_WIDTH, LCD_HEIGHT);

    /* Solid color display */
    GUI_SetBkColor(GUI_WHITE);
    GUI_Clear();

    WM_Exec();
    GUI_SetColor(GUI_BLACK);
    GUI_DispString("Hello World");

    for (int y=0; y< FIELD_HEIGHT; y++){
    	field[y][0] = field[y][FIELD_WIDTH-1] = 1;
    }
    for(int x=0; x < FIELD_WIDTH; x++)
    	field[FIELD_HEIGHT-1][x]=1;

    display();


}

この状態でビルドすると、uTickIntFlag;が定義されていないと出ると思います。このフラグは、LPC54102のマイクロtickタイマーを利用した時のフラグなので、削除して問題ありません。

このままだとコンソールのアスキーアート出力になっています。そこでPrintfの部分は全て消して、emWimのグラフィックを出力するAPIにします。

main()内のコードは、テトリスのフィールドとなる壁を表示するため、field[]という配列でブロックの有無を管理して、最後にdisplay()関数で画面を表示しています。

display()関数内でbitmapファイルを出力するには、GUI_BMP_Draw(const void * pBMP, int x0, int y0)を使用しています。

最初の引数はbitmapデータへのポインタです。まずは灰色のブロックの壁を表示したいと思うので*grayを指定しています。

そして、x, yの位置には直接表示位置を指定してあげる必要があります。

LCD画面サイズ全体をフィールドにしたいので、LCD横480ピクセルをゲーム画面の縦(20ブロック)として、そしてLCD縦272ピクセルをゲーム画面の横(12ブロック分)としたいので、LCD画面サイズをそれぞれ12と22で割ってブロックのX, Yの位置の単位とします。

ということで、とりあえず、下記マクロも設定しておきます。

int cell_xSize = LCD_HEIGHT/12;
int cell_ySize = LCD_WIDTH/22;

そして、display()関数でブロックを表示する位置x,yの位置に、このブロックサイズ分だけどんどんインクリメントされるようにします。

最終的に、次のように書き換えてみます。

void display(){
	//utickIntFlag = false;
	/* Screen clear  */
	//PRINTF("\033[2J");   	//Escape Sequence Clear screen
	//system("cls");
	//PRINTF("\033[1;1f");	//Cursor moves to (1,1)
	memcpy(dispBuffer, field, sizeof(field));
	for (int y =0; y< MINO_HEIGHT; y++)
		for(int x =0; x < MINO_WIDTH; x++)
			dispBuffer[minoY +y][minoX+x] |= minoShapes[minoType][minoAngle][y][x];/* Drawing */
	for (int y =0; y < FIELD_HEIGHT; y++){
		for(int x=0; x<FIELD_WIDTH; x++){
			
		    if (dispBuffer[y][x]){
		        //PRINTF("");
                        GUI_BMP_Draw(gray, y * cell_ySize, x * cell_xSize);
		    }else{
		        //PRINTF("  ");
		    }

		}
	    //PRINTF("\n\r");
	}
}

どうでしょうか?テトリスの壁が表示されたと思います。

ブロックもちょうど良い大きさになっていますよね。

でも、なんか表示が遅くない?

ブロックを表示させてみると、上からバラバラと順番に表示されて、全部表示されるまでに3秒ちょっと掛かってますよね。

今後、1秒毎にブロックが落ちてくるようにしたいのと、どんどんテトリミノが積み上がっていくと表示するテトリミノが多くなって、画面全体を表示する速度が遅くなっていくと思うんです。

これじゃ、ダメです。失敗です。

こんな表示が遅いのでは、ゲームになりません。

ん〜〜どうしようか?

「メモリーデバイス」機能

フレームバッファとは違うんだけど、フレームバッファ的なバッファにテトリミノを書き込んでから、バックバッファをフロントに移すみたいなことが出来ないかな〜と思っていたところ、emWinには「メモリーデバイス」という機能があり考えていた機能が実現できるみたいです。

フレームバッファは、動画やアニメーションなどの表示で使用することで、チラつきなどを抑えることができる物ですが、今回はそれとはちょっと違うんですよね。

で、早速メモリーデバイス機能を実装

メモリーデバイスは、フレームバッファとよく似ていて、画面に映し出す前のバッファに画面の描画を行って、描画が完了したら任意のタイミングで画面に出力することが出来ます。

以下使い方です。

/*①初めにメモリデバイスのハンドルを作成(定義をしておく)しておく*/
GUI_MEMDEV_Handle hMem;
/*②ハンドルに画面サイズを指定して作成する*/
hMem = GUI_MEMDEV_Create(0, 0, 480, 272);
/*③次に、そのハンドルをメモリデバイスとして選択する。これ以降、画面描画をするとバックバッファに描画することになる*/
GUI_MEMDEV_Select(hMem);
/*④画面をクリアする(バックバッファ)*/
GUI_Clear();

⑤画面の描画を行う

/*⑥「0」を指定してバックバッファ選択解除*/
GUI_MEMDEV_Select(0);
/*⑦最後にLCD画面に表示する*/
GUI_MEMDEV_CopyToLCD(hMem);

これでどうだ!

int main(void){
  : //長いので省略してます。
  :
  hMem = GUI_MEMDEV_Create(0, 0, 480, 272);

    for (int y=0; y< FIELD_HEIGHT; y++){
    	field[y][0] = field[y][FIELD_WIDTH-1] = 1;
    }
    for(int x=0; x < FIELD_WIDTH; x++)
    	field[FIELD_HEIGHT-1][x]=1;

    display();
}

void display(){
    //utickIntFlag = false;
    /* Screen clear  */
    //PRINTF("\033[2J");   	//Escape Sequence Clear screen
    //system("cls");
    //PRINTF("\033[1;1f");	//Cursor moves to (1,1)
    GUI_MEMDEV_Select(hMem);
    GUI_Clear();
    memcpy(dispBuffer, field, sizeof(field));
    for (int y =0; y< MINO_HEIGHT; y++)
	for(int x =0; x < MINO_WIDTH; x++)
	    dispBuffer[minoY +y][minoX+x] |= minoShapes[minoType][minoAngle][y][x];

	/* Drawing */
	for (int y =0; y < FIELD_HEIGHT; y++){
	    for(int x=0; x<FIELD_WIDTH; x++){
		//if (field[y][x]){
		if (dispBuffer[y][x]){
		    GUI_BMP_Draw(gray, y * cell_ySize, x * cell_xSize);
		    //PRINTF("◽");
		}else{
		    //PRINTF("  ");
		}
	    }
	    //PRINTF("\n\r");
	}
	GUI_MEMDEV_Select(0);
	GUI_MEMDEV_CopyToLCD(hMem);
}

ん?
なんか変わった?
って、何も変わってませんよね。
なんでだろう。。。
デバッグしてみると、どうやらハンドルの作成でエラーになっていて、バックバッファが作成されておらずメモリデバイスの機能は有効になっていないようです。

通常は正しく作成されると0以外の値が返ってきますが、何度やっても0が返ってきます。
ん〜〜〜

で、わかりましたよ。

どうやらemWin用のメモリ割当てサイズを超えているようです。
プロジェクトの「board」フォルダにある「emwin_support.h」内でメモリの割当サイズをしてしています。
デフォルトで2000(2kB)しか割当ててないので、今回は272x480x2Byte=255kBなので、280000Uにしました。

emwin_support.hのメモリ割当指定
emwin_support.hのメモリ割当指定

これで上手く表示できるようになりました。

一斉にテトリミノが画面に表示されるだけで、描画速度そのものは変化なさそう。。。そりゃそうですよ。裏で書くのと表で書くだけの違いですからね。

失敗です。。。トホホ。。。

Bitmapのデータを描画するにはかなり時間が掛かるようです。bitmapデータは、キャッシュに入っているはずなので、QSPIフラッシュからのアクセスペナルティは無いはずなんですが。描画自体に何かしらの時間が必要なんだと思います。

ゲームなどの画面リフレッシュや描画が多い場合には、Bitmapデータは扱ってはいけないということでしょうか。

別の方法を考えないといけません。

今回はここまでです。

次回は、Bitmapデータの描画に時間が掛かるので、別の方法でテトリミノの描画を行います。

なんだ、結局、失敗かよって思いますよね。ごめんなさい。失敗です。

次回から挽回です。お楽しみに。