[LPC54102] マイコンのコンソールでテトリスを作ってみた


今回は、マイコンのコンソール、いわゆるUARTを使ったターミナル出力を使って「テトリス」を作ってみました。

使用した言語は、組み込みなのでもちろんC言語を使用しました。

「作ってみた!」と大層なことを言っていますが、ゲームのプログラミングを勉強してみようと思って、今回は「ゲヱム道館」さんのYouTube「1時間で作ってみた」シリーズの「テトリス小一時間で作ってみた」を参考にしてみました。

参考と言っても、ほとんどそのものだったりします。。。

ゲヱム道館さんは、WindowsのVisual C++を使用して、Windowsのコンソールアプリとして開発していますが、私はマイコン好きなので、もちろんマイコンで実装しました。

ゲームの作成について解説する程ゲーム開発の知識もありませんので、テトリス実装についての解説はゲヱム道館さんにお任せします。

MCU上に実装する際に問題になった点や工夫を私なりに説明したいと思います。

使用したボード

LPCXpresso54102

LPC541xx MCU Family Block diagram
出典:NXP Semiconductors LPC541xx MCU Family

使用したボードは、NXP社のLPCXpresso54102というボードで、実装されているLPC54102はデュアルコアMCU(Cortex-M4F・Cortex-M0+)です。プログラムを実行するのは、メインコアのCortex-M4Fコア上に実装しました。

サブコアには、Cortex-M0+が採用されています。

この製品は、センサーデータの取得のような定期的なアクセスには、省電力コアのCortex-M0+を使用することでシステムの消費電力を下げつつ、センサーデータのアルゴリズム処理にはパワフルなCortex-M4Fを使うことで負荷が高い処理にも対応できるアーキテクチャになっています。

今回は、ただ単純にコンソールだけしか使っていませんので、こんなMCUを使う必要は全くなかったんですけどね〜〜

LPC541xxシリーズじゃなきゃ出来ないわけではなく、他のマイコンでも同様に実装可能なので、皆さんも是非参考にしてご自身のマイコンでゲーム開発を楽しんでみてください。

おすすめボード

以下におすすめボードを紹介します。Amazonで買えるものを紹介しますが、この他NXP社(www.nxp.com)のBuy Direct(オンラインショッピング)で様々なボードが買えます。

もちろん、電子部品を取り扱う通販でも購入が可能です。

Kinetis FROMボード(KL27、K64など)

FRDM-K64F

LPCXpressoボード(LPC802、LPC11U37や LPC5411xなど)

開発環境IDE

統合開発環境は、完全に無料で使用できるMCUXpresso IDEを使わせてもらいました。私はMac使いなので、MCUXpresso IDEはMac環境でも対応しているのが

MacOSでも使える開発環境は重宝します。

MCUXpersso IDEは、こちらからダウンロード可能です。

MCUXpresso IDEページ

Hello Worldプロジェクトのインポート

さて、いよいよ「テトリス」の実装をしていきます。まず、プロジェクトの作成から始めます。

プロジェクトは、スクラッチから始めるより、スタートアップ処理や基本的なクロック、ハードウェア初期化処理などが既に設定されている「Hello World」プロジェクトを使います。

マイコン開発を始める必要最低限の設定を行った状態で開発を始めることが可能です。

「Hello World」プロジェクトのインポート

プロジェクトインポート

次に、ボードを選択します。今回わたしは、LPC54102を選択していきます。インポートしたSDKのデバイスが表示されます。

Hello Worldプロジェクト

上の画面は、「Hello World」プロジェクトをインポートしたところです。このプロジェクトにコードを足していきます。

ターミナル出力設定

今回は、ホストPC上のターミナルにアスキーアートを出力してゲームを作成します。

まずは、ターミナルにコンソソール出力することが、マイコンのコンソールゲームを開発する上での第一関門です。

Macでコンソールを表示するには?
私の環境はMacOSなので、ターミナルアプリを使用し「Screen」コマンドでコンソール表示を行っています。

「Screen」を使用するには、ボードをUSBコネクタに接続し、MacOSのターミナルを起動後に以下コマンドを実行します。

$ ls /dev/tty.usb*
/dev/tty.usbmodem1422  <——-メモなどして覚えておくこと
$ screen /dev/tty.usbmodem1422 115200 <— メモしたパスを書き込む

screenに続けてメモしたのを記入します。また、UART接続のボーレート(115200bps)を指定します。これで、Macのターミナルで、コンソールとして使用することが出来ます。

MCUXpresso IDEにもコンソール表示機能がありますが、このMCUXpressoのコンソールではテトリスが上手く動きませんでしたので、UART出力をしてTeraTermなどのターミナルを使用することをおすすめします。(あ、すみません。TeraTermは、まだ試してないので正しく動作するかわかりませんが、恐らく問題ないかと思います。MacOSのScreenでは問題ありませんでした。)

上手く動かないのは、エスケープシーケンスが認識されないためです。

MCUXpresso IDEではコンソールへの出力設定を、「セミホスティング」と「UART出力」をどちらか選択することが出来ます。

外部ホストのコンソールに出力するためには、「UART出力」を選択する必要があります。

セミホスティングについて知りたい方は、以前の記事を参考にしてください。

MCUXpresso IDE 10.2.1では、このセミホスティングとUART出力の設定を、クリック一発で簡単に切り替えることができるようになっています。

UART出力への切り替え

今回は、外部ホストのコンソールを利用するために、UART出力へ切り替えます。切り替えるためには、Quick Settingから設定します。

左上側のプロジェクトウィンドウ内にあるプロジェクトをクリックして選択した状態で、

Quick Setting – SDK Debug Console – UART consoleを選択します。

注意 プロジェクトを選択しておかないと、「UART Console」が選択出来ません。

選択後、「トンカチマーク」をクリックして、再ビルドします。ビルド後にデバッグを開始して「”Hello World”」が出力されるか確認します。

「Hello World」を確認

コンソール出力

これで、無事コンソールへの出力ができるようになりました。

コードの実装

コードを実装して行く中で、マイコン上に実装する時に問題になる点があります。その問題を順を追って説明していきます。

MCUで実装する際の問題点

1. 画面のクリア

まず、マイコンでコンソールゲームを作成する際の問題点は、画面描画のため定期的に画面をクリア(消去)する必要あるという点です。

クリアせずに画面を描画しようとすると、当然どんどん行は下に出力され下にスクロールして行くし、刻一刻と時間が過ぎる中でキャラクタの動きを与えると、画面が前の描画との不整合からチラつきが発生したりします。

そこで、定期的に画面を一旦消去して、画面の描画をし直すんです。

ゲヱム道館さんは、Visual C++のC標準ライブラリ(stdlib.h)のシステムコマンド”CLS”を使用して、画面をクリアしています。一方、マイコンではこのコンソール画面のクリアコマンドがありません。このSystem(“cls”)で、一応、Buildは通りますが、画面はクリアされないんです。

では、マイコンでは、どうするか?

エスケープシーケンスでクリア

マイコンでコンソール画面をクリアするには、「エスケープシーケンス」コマンドをコンソールに出力し、コンソール画面を消去することが出来ます。

今回画面クリアで試した「エスケープシーケンス」は、以下2つになります。

\033[2J 画面全体 を消去
\033[1;1f  カーソルを(1,1)に移動する。

実は、エスケープシーケンスで画面クリアを入れると、当然一旦画面がクリアされるのですが、UART出力する時間が掛かるのと、画面クリアのタイミングで若干のチラつきが発生するんです。

Hello Worldを出力すると、Hello Worldを出力し終わる前に、画面クリアされてしまうことがあるようです。

カーソル位置を描画開始位置に移動させる

そこで、画面の安定描画をさせるために、私のプログラムではカーソルを元の描画スタート位置に移動させています。

それが、\033[1;1fこのエスケープシーケンスです。

画面クリアせずに、カーソルだけを描画開始位置に移動させて描画をさせることで、変更があったブロックだけが変化するように見えるので、描画が安定して見えます。

チラつきが劇的に少なくなります。

インターネットで「エスケープシーケンス」と検索すると色々出てきますので、興味ある方は調べてみてください。

2. 画面描画タイミング

テトリスでは、テトリミノ(テトリスのブロック)が落ちるタイミングが約1秒ごとに下に向かって落ちて行きます。この1秒のタイミングで画面描画を行います。

ゲヱム道館さんでは、C標準ライブラリを使ってシステムの時刻を取得して1秒ごとに画面描画をすることで対応しています。

マイコンではタイマーを使うことで同様のことを実行することが可能です。

例えば、RTC(Real Time Clock)機能を使用して時刻を取得する(初期化の時に現在時刻の設定が必要)か、定期的な割り込み機能(LPCでは:UTICK(Micro Tick Counter), Kinetisでは: PIT( Periodic Interrupt Timer))や汎用のタイマーを使っても実装可能です。

今回は、LPCのUTICK(Micro Tick Counter)を使用することにしました。理由は、初期化や設定が非常に簡単で、使用するまで時間が掛からないからです。

UTICKドライバの追加

まず、UTICKドライバを追加します。

MCUXpresso IDE画面の左上側にあるプロジェクトウィンドウ内にある、プロジェクトを選択した状態で右クリック(Macならコントロール&クリック)、そして「Manage SDK Components」をクリックします。

プロジェクトオプション

Manage_SDK_components

add_utick_driver

UTICKドライバをチェックして、OKをクリックすると、ドライバが追加されます。

UTICKの初期化

ソース一番上に、#include “fsl_utick.h”を追加して、下記のように初期化をします。

UTICKの初期化では、引数にUTICKモード、カウント数、コールバック関数をしています。カウント数を600000にすると、約1秒で割り込みが発生します。

utickIntFlag = false;
UTICK_Init(UTICK0);
UTICK_SetTick(UTICK0, kUTICK_Repeat, 600000, cb_utick);

割り込み発生後に、指定したコールバック関数をコールしてくれます。
コールバック関数では、割り込みフラグのクリアとユーザ変数としてのフラグをTrueにするだけですが、このフラグ(utickIntFlag)を見て1秒経ったかどうかを判断します。
コールバック関数は、

void cb_utick(){
	UTICK_ClearStatusFlags(UTICK0);
	utickIntFlag=true;
}

試しに、このutickを使って見ます。

while(1)ループ内に、先ほどのutickIntFlagをポールリングして、utickの割り込みがあった時に「”Hello World”」を出力します。
printfで出力が終わった後に、utickIntFlagをfalseに戻しておきます。

    while(1){
    	if(utickIntFlag){
    	//PRINTF("\033[2J");   	//Escape Sequence Clear screen
    	PRINTF("\033[1;1f");	//Cursor moves to (1,1)
    	PRINTF("hello world.\r\n");
    	utickIntFlag = false;
    	}
    }

実際に、これでデバッグを実行してみると、カーソル位置が(1,1)に移動して、1秒ごとにHello World出力しています。(書き換わっているのは分かりませんが、多分書き換えが出来ています。)

先ほどの、出力画面と変わりありませんが、カーソル位置が左上に移動しているのが分かると思います。
定期的な出力

3. アスキーアートが上手く表示出来ない

私の環境(MacOSX)が問題だったのか、テトリスのブロックやフィールドを描画するのに、アスキーアートを使用していますが、これが上手く描画されませんでしたね。

四角のブロックを表示するのですが、小さすぎたり、ブロックとブロックの間の間隔が広くなってしまったりと、上手く描画できませんでした。

結果的には、◾️の隣に余計な文字が付加されているようです。これを削除することで、上手く描画させることが可能になりました。

IDEのテキストエディタ上で、「◾️」と表示すると、カーソルを動かしてみると分かるのですが、2文字と認識されているようです。四角の隣に見えない文字が設定されているようで、それが原因で、コンソール上では四角と四角の間に空白が挿入されて表示され、間隔が広くなってしまっていたようです。

4. キーボードhitの認識

これは、問題という訳ではありませんが、NXPのMCUXpresso SDKではkbhit()のような、キーボードが押されたことを認識する関数は用意されていないので、UARTのRXステータスレジスタを直接見てデータがあれば(RXREADY)、処理するというコードで代用しています。

if (USART0->STAT&USART_STAT_RXRDY_MASK){

「テトリス」のコードを実装する

あとは、ゲヱム道館さんのYouTubeに沿ってコーディングを進めていきます。

私のソースコードは、Gitに公開していますので、上手く動かない場合には、参考にして見てください。

テトリスのゲームクリアは、実装していませんので、延々とブロックが落ちてきて、ブロックをひたすら消すだけのゲームとなっています。

完成!!

私のソースコードはこちらから→

https://github.com/mcuthings/LPC54102-Tetris

まとめ

今回は、マイコンでコンソールゲーム「テトリス」を作って見ました。ゆくゆくは、コンソールではなくLCD画面にテトリスをカラーで作って見たいなーと思っています。いつになることやら。。。

ゲーム開発で使うC言語は、ゲーム特有のコーディングがあり、少々難しいところもありますが、面白いですね。

是非、皆さんもチャレンジして見てください。勉強になりますよーーー。