2020年6月4日木曜日

ゲームボーイの画面にアニメーションを表示する

おもしろ半分でParty Parrotをゲームボーイで動かしたら、結構な数の皆さんに回覧いただきました。せっかくなので、仕組みをまとめておきます。

実装までの流れ

次のフローで組み立てていきます。

  1. 絵を用意する
  2. ゲームボーイの表示データに変換する
  3. 順番に表示する

絵を用意する

Party ParrotはWikipediaのものを使いました。gifアニメを各コマに分解し、4階調に変換します。これはimagemagickを使いました。

この白黒画像を、ゲームボーイの画面表示に合わせたビット列に変換していきます。

ゲームボーイの表示データに変換する

任意の絵をゲームボーイにする方法が例示されているので、それに従います。リンク先の解説でいくつか挙げられている注意点がさらに厳しくなるので気をつけてください。

  • 8x8サイズのパーツが255種類に収まるように絵を描きます→127種類に収めてください
  • Pic2Tilesで生成されたC言語のファイルからconstを外すよう記載がありますが、一部を除き外してはいけません。

パーツを127種類に収める点について。1枚絵の表示であれば255種類すべて使えるのですが、アニメにする関係で半分に制限されます。1枚表示している間にもう一方の絵を用意し、整ったところで一気に表示を切り替える方法をとっているため、このもう一方を用意するために半分ずつしか使えないということになります。

constを外してはいけない点について。現時点のGBDKではコンパイルエラーが出なくて問題がないことと、constなしでは起動時にRAMに展開されるために今回のように10コマ使用すると、5コマ以上でRAMがオーバーフローするためゲームボーイが落ちます。画像のデータそのものは動的に変化する必要がないので、基本的にconstのままROMにとどめておくようにしてください。例外はmap.Cのうち偶数枚目のコマのものです。変換時に生成されるCファイルには「タイル内のデータ」と「どの位置にどのタイルを並べるかを定義したデータ」が含まれています。後者のデータは常にタイル内データが専用の領域の先頭から並んでいる前提で生成されるので、偶数コマのデータは128個分のオフセットが必要になります。あらかじめ数字を書き換えておくか、constを外してゲームの起動時に初期化の一環として一気に書き換える必要があります。今回のParty Parrotでは面倒だったので起動時に書き換えましたが、コマが多かったり他の用途でRAMを使っていたりで容量が逼迫しているとconstのまま書き換えておいたほうがよいです。

順番に表示する

GBDKに用意されているタイル表示用の関数を使います。基本的に下記のコードを繰り返すだけです。

set_bkg_data(0,100,TileData_pp0); set_bkg_tiles(0,0,20,18,MapData_pp0); delay(50); set_bkg_data(100,100,TileData_pp1); set_bkg_tiles(0,0,20,18,MapData_pp1); delay(50);

set_bkg_dataでタイル内のデータを専用領域にコピーします。第1引数の数字がコピー位置のオフセットです。Party Parrotの場合はどのコマもタイル数が100以下だったので100としています。set_bkg_tilesは、指定した範囲をマップデータに従って書き換えるものです。

次のコマのタイルデータを書き込む際、多少時間を空けないと表示が崩れることがあったのでdelayを入れています。

さいごに

ポケットカメラの撮影時プレビューを見ると、明らかにタイルデータでは間に合わなさそうなドット画像の動画が表示されているので、まだ改善の余地はあるのかもしれません。この記事のやりかたでも、タイルとマップデータをカートリッジに入りきるだけ用意すれば、それなりの時間のアニメーションを表示させることもできるなど、可能性は残しています。

2020年5月16日土曜日

Nordic SDKのサンプルコードにないHIDサービスを実装する

Nordic nRF5 SDKには、HIDの実装例としてキーボードとマウスが公開されています。HIDはそれ以外の使われ方も定義されていますが、サンプルは公開されていないうえインターネットでも実装例は少ないです。Gamepadの実装をやってみたところいろいろな苦労があったので、参考のためにまとめておきます。

マウスを参考にして、USBのHIDを載せる。

フォーラムに書いてあります。基本的に、次の流れで調整すると動きます。

  1. レポートディスクリプタの中身を整える (コード内ではrep_map_data[]に定義)
  2. レポートディスクリプタに記載したルールに従ってバイト列を作る
  3. データを送信する (コード内ではble_hids_inp_rep_send()関数で送信)

レポートディスクリプタの中身を整える

レポートディスクリプタは、送信したビット列のデータをどのように解釈するかを定義したものです。なので、中身は自分で必要なものをきちんと定義する必要があります。Gamepadの場合はどのように指定すればいいのかは、下記のリンクが参考になりました。

今回は下記のように指定しています。ゲームボーイのキー操作を入力としたいので、8ビットの入力を4つのボタンと1つの方向キーとして扱います。

static uint8_t rep_map_data[] ={
    0x05, 0x01,             // USAGE_PAGE (Generic Desktop)
    0x09, 0x05,             // USAGE (Gamepad)

    0xa1, 0x01,             // COLLECTION (Application)

    0x05, 0x01,             // USAGE_PAGE (Generic Desktop)
    0x09, 0x39,             // USAGE (Hat switch)     // 十字キー
    0x15, 0x00,             // LOGICAL MAXIMUM (0)    // このデータは0〜7の範囲
    0x25, 0x07,             // LOGICAL MAXIMUM (7)    
    0x35, 0x00,             // PHYSICAL MINIMUM (0)   // 0〜7が
    0x46, 0x3b, 0x01,       // PHYSICAL MAXIMUM (315) // 0〜315に対応
    0x75, 0x04,             // REPORT SIZE (4)        // SIZEとCOUNTで
    0x95, 0x01,             // REPORT COUNT (1)       // 4ビットが1個の意味
    0x65, 0x14,             // UNIT (Degrees)         // 単位は角度
    0x81, 0x42,             // INPUT (DATA, VARIABLE, ABS, NULL)

    0x05, 0x09,             // USAGE_PAGE (Button)    // ボタン単体
    0x15, 0x00,             // LOGICAL MINIMUM (0)    // このデータは0〜1の範囲
    0x25, 0x01,             // LOGICAL MAXIMUM (1)
    0x19, 0x01,             // USAGE MINIMUM (1)
    0x29, 0x04,             // USAGE MAXIMUM (4)
    0x75, 0x01,             // REPORT SIZE (1)        // SIZEとCOUNTで
    0x95, 0x04,             // REPORT COUNT (4)       // 1ビットが4個の意味
    0x81, 0x02,             // INPUT (DATA, VARIABLE, ABS)

    0xc0,                   // END_COLLECTION
};

ルールに従ってバイト列を作る

レポートディスクリプタで指定したルールに従った値を並べます。レポートディスクリプタに記載した順序がそのままバイト内のビット列になるので、下位4ビットが十字キー・上位4ビットがボタン単体となるよう組み立てます。

データを送信する

組み立てたデータは、関数ひとつで送信できます。

err_code = ble_hids_inp_rep_send(&m_hids,
                                 INPUT_REP_BUTTONS_INDEX,
                                 INPUT_REP_BUTTONS_LEN,
                                 buffer);

  • INPUT_REP_BUTTONS_INDEX: マウスのサンプルのように、レポートディスクリプタのなかに複数のIDがある場合はここで指定します。今回の例ではひとつしかないので、0になります。
  • INPUT_REP_BUTTONS_LEN: 送信するデータのバイト数です。今回は1バイトなので1です。
  • buffer: データ本体のポインタ。

動作例

データの受信側で確認する方法

HTML5 Gamepad Testerを使っています。ペアリングし、レポートディスクリプタの指定もうまくいっている場合は、キー操作に反応します。

ペアリングできていてもキー操作に反応しない場合、レポートディスクリプタの記載内容がきちんと合っていない可能性があります。バイト列が送信されているかどうかは、Macを使っていると確認が楽です。Appleの開発ツールとして公開されているPacket Loggerを使うと、どのデバイスからどのデータが飛んでいるかを確認できます。バイト列の中身をPacket Loggerで確認できれば、ペアリングと通信はうまくいっているので、おかしいのはディスクリプタだと推測できます。

2020年2月8日土曜日

ポケットカメラの画像をM5Stackに送信して表示する

前回の記事で、ポケットカメラの画像をPCに送信することができました。でもこれではPCとNucleoボードが必要で、若干大掛かりだなと思っていました。これをM5Stackで受信できれば、画面に映せるし無線LANでTwitter等にアップロードも可能だなと思い、移植してみました。

M5Stackと通信ケーブルを接続する

ゲームボーイの通信ケーブルの信号線は5V駆動なので、5VトレラントではないM5Stackでは、とりあえず分圧抵抗で降圧します。M5Stack→ゲームボーイの方向は、昇圧せずとも動くことはカートリッジ自作の際にわかっているのでとりあえずそのままにしました。

M5Stackに接続する際、当初は向かって上側(21〜23のピン)に接続していたのですが、23番が内部でディスプレイ制御に使われているのに気付くまでディスプレイが動かないと困りました。GPIOとしても使用できますが、ディスプレイ初期化後にGPIOとして設定してしまうため、それ以降はディスプレイの信号を受け付けなくなります。

M5Stackでデータを受信し画像として表示する

データ受信までは、DholeさんのSTM32側コードの移植で済みました。移植の際、Arduinoではうまく動かず、ESP-IDFの環境を使っています。データ受信後、ドット情報を抽出しビットマップに整えてから画面に表示するのですが、表示の際はM5.Lcd.drawBitmap()が必要になり、ESP-IDFにArduinoライブラリを追加しました。バージョンが異なるとうまく動かないらしく困りましたが、こちらの記事が参考になりました。結局、この方のリポジトリをクローンしてアプリを作っています。

動作の様子

2020年1月13日月曜日

ポケットカメラの画像を取り出す

かつてゲームボーイのカートリッジからカメラが伸びているポケットカメラというソフトがありました。撮影した画像は通信ケーブルを使い、ポケットプリンタからロール紙に印刷するという形で現像していましたが、印刷部を適宜加工することで画像を他環境へ転送することができます。例によってDholeさんの実装例があったので、まずはそれを後追いして検証しました。

動作までの流れ

下記のフローで実装を行いました。

  1. 通信ケーブルを改造する
  2. 疑似ポケットプリンタ基板を実装する
  3. 基板が出す画像データを受け取るアプリを実装する

通信ケーブルを改造する

通信ケーブルのデータを基板で受信するために、通信ケーブル本体を切断しています。通信ケーブル用のソケットがAliexpress等で購入できればよかったのですが、初代ゲームボーイ用の大きいサイズはさすがに作られていないようで、泣く泣くカットしました。配線はGNDを含めて4本なので、秋月電子のUSBコネクタDIP化基板を利用して扱いやすくしました。

疑似ポケットプリンタ基板を実装する

ポケットプリンタのフリをして、ゲームボーイ側には正しい応答を返し続け、PC側に画像信号を転送する基板を実装します。基本的にDholeさんのリポジトリのコードを使用しますが、ヘッダファイルだけがなかったりフレームワークの内容に変更があったりするのでその点だけ修正しました。修正後のリポジトリはこちらです。例ではNucleo-F411REでしたが、手元にあったのはF401REだったのでクロックを遅くするよう設定しています。

基板が出す画像データを受け取るアプリを実装する

こちらもDholeさんのサンプルを使用します。基板がシリアル通信でデータを渡すので、それを受信し画像に変換して保存します。

動かしてみる

疑似プリンタ基板が接続されているPCで受信アプリを起動し、ポケットカメラ側で印刷を実行します。各部が正常に接続・動作している場合、ゲームボーイの画面はデータ送信中となり、PCに画像が保存されます。

印刷される画像そのものが出てくるので、写真+フレームのPNG画像で保存されます。