+===========+ |P/ECE研究室| +===========+ P/ECE研究記録 2002年 ==================== * Wed Dec 25 00:00:00 JST 2002 Naoyuki Sawa - ISDマニュアル#3 (前回からの続き…) 今回は、残り三つのファイルシステムコマンドについてです。         =r    ファイルを読み出します。(P/ECE→PC)         =d    P/ECE上のファイルを削除します。         =i    ファイルシステムを初期化します。 しかしながらこれら三つのコマンドは、前回の二つに較べてかなり使用頻度が低いです。 例えば僕の場合、実験以外で「=r」「=d」は使ったことがなく、「=i」は一回使っただけです。 ですので、これら三つのコマンドについてはざっと流してしまうことにしましょう。 コマンド「=r」と「=d」でのファイル指定に関するルールは、コマンド「=w」の場合と同じなので、 コマンド「=r」と「=d」に関する今回の説明は、前回のコマンド「=w」の説明とほとんど同じです。 実際、今回の説明の大部分は前回のコピーなのですが(^^;、念のため、各入力例の動作確認は行いました。 ------------------------------------------------------------------------------------------------------------------------------- -------------------- =r:ファイル読み出し -------------------- 書式:     =r <ファイル名> P/ECE上にあるファイルを、PCに読み出します。 PC上に既に同名のファイルが存在した場合は、既存のファイルに上書きします。 例1:     =r date.pex         P/ECE上にある「date.pex」を、PCの現在の作業フォルダに「date.pex」というファイル名で読み出します。         読み出しに成功すると、次のような出力が画面に表示されます。         reading 'date.pex' done         date.pexが存在しない場合は、読み出しに失敗します。         画面には何も表示されず、すぐに次のコマンド待ちプロンプト「>」が表示されます。 例2:     =r date         P/ECE上に「date.pex」があれば、PCの現在の作業フォルダに「date.pex」というファイル名で読み出します。         ファイル名の拡張子を省略すると、拡張子「.pex」が自動的に補われます。         もしP/ECE上に拡張子なしの「date」というファイルがあり、これを読み込みたかったのだとしても、ISDが「.pex」を補ってしまいます。         従ってISDの「=r」コマンドでは、拡張子のないファイル名を持つファイルを読み出すことはできません。         拡張子なしのファイルを読み出そうとして失敗した様子を、次に示します。         C:\>isd                                  PIECE Monitor version 1.07 (C)2001-2002 by MIO.H             >=                           …P/ECE上のファイルリストを取得します。         + 0: 2703 startup.pex                          + 1: 1221 date                    …拡張子なしの「date」というファイルがあります。          85 sectors (348160 bytes) free                    >=r date                        …「date」を読み出してみます。         >                            …ISDは「date.pex」を読み出そうとして失敗し、何も表示されません。 例3:     =r readme.txt         「.pex」以外の拡張子を持つファイルを読み出す場合は、拡張子を省略することはできません。         必ず、拡張子まで含めて指定してください。 例4:     =rdate.pex         コマンド「=w」の場合と同じく、「=r」とファイル名の間の空白文字は、あってもなくても構いません。         この例のように、「=r」とファイル名を続けて書いても大丈夫です。 以下は悪い例です。 例5:     =r hello.pex world.pex         複数のファイルを一度に読み出すことはできません。         この例の場合、「hello.pex」は読み出されますが、「world.pex」の指定は完全に無視されます。         すなわち、         =r hello.pex         と指定したのと全く同じ結果となります。         「world.pex」を指定したことに対するエラーや警告等も表示されません。 例6:     =w *.pex         ワイルドカードによるファイル指定はできません。 ---------------- =d:ファイル削除 ---------------- 書式:     =d <ファイル名> P/ECE上にあるファイルを削除します。 例1:     =d date.pex         P/ECE上にある「date.pex」を削除します。         削除に成功すると、次のような出力が画面に表示されます。         FlashErase c28000 0         FlashWrite c28000 130000 1000 0         date.pexが存在しない場合は、削除に失敗します。         画面には何も表示されず、すぐに次のコマンド待ちプロンプト「>」が表示されます。 例2:     =d date         P/ECE上に「date.pex」があれば、「date.pex」を削除します。         ファイル名の拡張子を省略すると、拡張子「.pex」が自動的に補われます。         もしP/ECE上に拡張子なしの「date」というファイルがあり、これを削除したかったのだとしても、ISDが「.pex」を補ってしまいます。         従ってISDの「=d」コマンドでは、拡張子のないファイル名を持つファイルを削除することはできません。         拡張子なしのファイルを削除しようとして失敗した様子を、次に示します。         C:\>isd                                  PIECE Monitor version 1.07 (C)2001-2002 by MIO.H             >=                           …P/ECE上のファイルリストを取得します。         + 0: 2703 startup.pex                          + 1: 1221 date                    …拡張子なしの「date」というファイルがあります。          85 sectors (348160 bytes) free                    >=d date                        …「date」を削除してみます。         >                            …ISDは「date.pex」を削除しようとして失敗し、何も表示されません。 例3:     =d readme.txt         「.pex」以外の拡張子を持つファイルを削除する場合は、拡張子を省略することはできません。         必ず、拡張子まで含めて指定してください。 例4:     =ddate.pex         コマンド「=w」や「=r」の場合と同じく、「=d」とファイル名の間の空白文字は、あってもなくても構いません。         この例のように、「=d」とファイル名を続けて書いても大丈夫です。 以下は悪い例です。 例5:     =d hello.pex world.pex         複数のファイルを一度に削除することはできません。         この例の場合、「hello.pex」は削除されますが、「world.pex」の指定は完全に無視されます。         すなわち、         =d hello.pex         と指定したのと全く同じ結果となります。         「world.pex」を指定したことに対するエラーや警告等も表示されません。 例6:     =w *.pex         ワイルドカードによるファイル指定はできません。 -------------------------- =i:ファイルシステム初期化 -------------------------- 書式:     =i P/ECEのフラッシュメモリファイルシステムを初期化します。 例:      =i         P/ECEのフラッシュメモリファイルシステムを初期化します。         確認のため、ISDから次のような問い合わせが表示されます。         Init file system. Sure?(Y/N)         本当に初期化してよければ「y」、やっぱりやめるなら「y」以外のキーを押します。         「y」キーを押すとファイルシステムが初期化され、次のような出力が画面に表示されます。         FlashErase c28000 0         FlashWrite c28000 130000 1000 0         「y」以外のキーを押して初期化を取りやめた場合は、なぜかファイル一覧が表示されます。         “初期化しなかったのでご安心を”というメッセージでしょうか?(^^; なにかの拍子にP/ECEのファイルシステムが壊れてしまった場合は、コマンド「=i」で回復できる可能性があります。 ただし、P/ECE上のファイルは全て消えてしまいますので、ご注意ください。 また、「P/ECEカーネル・アップデート」(WinPku.exe)や「P/ECEコミュニケータ」(WinIsd.exe)を使って、 うっかり2MB版P/ECEにカーネルアップデートをかけてしまった場合にも、コマンド「=i」のお世話になることになります。 「P/ECEカーネル・アップデート」や「P/ECEコミュニケータ」でカーネルアップデートを行うと、 512KB以降のセクタが“無効なセクタ”とマークされてしまい、せっかくの2MB版P/ECEが通常の512KB版P/ECEになってしまいます。 2MB版P/ECEに戻すには、「FLASH2MB向け改造カーネル及びシステム」のreadme.txtファイルに書かれているように、 2MB版カーネルを転送した後、2MB用ISDのコマンド「=i」でファイルシステムを初期化する必要があります。 2MB版カーネルを転送しただけでは、512KB以降のセクタは有効になりません! 僕も先日これをやってしまって、ちょっとハマリました。 参照:     「六波羅さんのスペース」(http://rokuhara.jp.org)         →「めっちゃ幸せFRASHメモリ4倍化計画」(http://www2s.biglobe.ne.jp/~rokuhara/piece/flash2mb.htm)         →「FLASH2MB向け改造カーネル及びシステム」(http://www2s.biglobe.ne.jp/~rokuhara/piece/flash2mb.lzh) ------------------------------------------------------------------------------------------------------------------------------- ファイルシステムコマンドは以上です。 次回は、メモリダンプ等の低水準コマンドについて見ていこうと思います。 (続きます…) * Sun Dec 22 08:26:00 JST 2002 Naoyuki Sawa - ISDマニュアル#2 (前回からの続き…) 今回は、ISDコマンドの中でも特に使用頻度が高いと思われる、ファイルシステムコマンドについて見ていきます。 その前に、コマンド説明全般に関する注意点です。 ISDは既に「入力オペレーションモード」でコマンド待ち状態になっているものと仮定して、説明を行うことにします。 ISDの起動・終了手順については、コマンド入力例に含めません。 例えば、次のようなコマンド入力例があった場合、         =d hello.pex         =w world.pex ISDの起動・終了手順も含めると、実際には次のようにタイプしてください。         isd         …「入力オペレーションモード」でISDを起動します。         =d hello.pex             =w world.pex             q          …ISDを終了します。 「入力オペレーションモード」については、前回の説明を参照してください。 ------------------------------------------------------------------------------------------------------------------------------- ======================== ファイルシステムコマンド ======================== ファイルシステムコマンドは、ファイル一覧表示やファイル転送など、ファイルシステムに関する操作を行うコマンドです。 全部で5つのコマンドがあります。         =     ファイル一覧を表示します。         =w    ファイルを書き込みます。(PC→P/ECE)         =r    ファイルを読み出します。(P/ECE→PC)         =d    P/ECE上のファイルを削除します。         =i    ファイルシステムを初期化します。 ご覧のように、ファイルシステムコマンドは全て「=」で始まります。 ところで、ISDのヘルプ(コマンド「?」で表示されます)を見ると、もう一つ「=」で始まるコマンドが載っています。 時刻設定コマンド「=t」です。 が、これはISDヘルプのミスプリントで、正しい時刻設定コマンドは「t」です。 時刻設定コマンド「t」については、次回以降に説明します。 --------------- =:ファイル一覧 --------------- 書式:     = P/ECE上にあるファイル一覧と、開きセクタ数・バイト数を表示します。 例1:     =         出力結果は次のようになります。         + 0: 2703 startup.pex         + 1: 92352 rt.pex         + 2: 11408 rt.fpk         + 3: 57400 padv.pex         + 4:137719 padv.fpk          11 sectors (45056 bytes) free ファイル一覧コマンドについては、特に注意点もありません。 ファイル一覧はP/ECEコミュニケータ等を使っても見られるのですが、ISDのファイル一覧コマンドも意外と便利です。 標準の開発環境を使ってP/ECEアプリケーションの作成を行う場合、ほとんどの作業はDOS窓での作業となります。 その際、ファイル一覧を見るためだけにP/ECEコミュニケータを起動しなくても、コマンドプロンプトから         isd = とタイプするだけで、P/ECE上のファイル一覧が調べられます。 DOSの「DIR」コマンドと同じ感覚ですね。 -------------------- =w:ファイル書き込み -------------------- 書式:     =w <ファイル名> PC上にあるファイルを、P/ECEに書き込みます。 P/ECE上に既に同名のファイルが存在した場合は、既存のファイルに上書きします。 例1:     =w date.pex         現在の作業フォルダにある「date.pex」を、P/ECEに書き込みます。         書き込みに成功すると、次のような出力が画面に表示されます。         date.pex         FlashErase c77000 0         FlashWrite c77000 130000 4c5 0         FlashErase c28000 0         FlashWrite c28000 130000 1000 0         date.pexが存在しない場合は、書き込みに失敗します。         ファイル名だけが画面に表示され、エラーメッセージ等は特に表示されないので要注意です。         date.pex         空き容量が足りなくて書き込みに失敗した場合は、ちゃんとエラーメッセージも表示されます。         date.pex         空きが足りません。 例2:     =w C:\usr\PIECE\app\date.pex         「C:\usr\PIECE\app\date.pex」を、P/ECEに書き込みます。         現在の作業フォルダ以外の場所にあるファイルを書き込むときは、この例のようにフルパスで指定してください。 例3:     =w date         現在の作業フォルダに「date.pex」があれば、「date.pex」をP/ECEに書き込みます。         ファイル名の拡張子を省略すると、拡張子「.pex」が自動的に補われます。         もし本当に「date」というファイルを書き込みかったのだとしても、ISDが「.pex」を補ってしまいます。         従ってISDの「=w」コマンドでは、拡張子のないファイル名を持つファイルを書き込むことはできません! 例4:     =w readme.txt         「.pex」以外の拡張子を持つファイルを書き込む場合は、拡張子を省略することはできません。         必ず、拡張子まで含めて指定してください。 例5:     =wdate.pex         実は、「=w」とファイル名の間の空白文字は、あってもなくても構いません。         この例のように、「=w」とファイル名を続けて書いても大丈夫です。 以下は悪い例です。 例6:     =w hello.pex world.pex         複数のファイルを一度に書き込むことはできません。         この例の場合、「hello.pex」は書き込まれますが、「world.pex」の指定は完全に無視されます。         すなわち、         =w hello.pex         と指定したのと全く同じ結果となります。         「world.pex」を指定したことに対するエラーや警告等も表示されません。 例7:     =w *.pex         ワイルドカードによるファイル指定はできません。 ファイル書き込みコマンドが役に立つのは、MakefileでpexをP/ECEに転送する場合です。 例えば次のような内容のMakefileがあった場合         all: hello.srf         pex: hello.pex         hello.srf: hello.c             pcc33 hello.c         hello.pex: hello.srf             ppack -e hello.srf -ohello.pex 「make」で「hello.srf」が作成され、「make pex」で「hello.pex」が生成されます。 P/ECEへのpexファイル転送も自動化するためには、次の二行を追加します。         all: hello.srf         pex: hello.pex         hello.srf: hello.c             pcc33 hello.c         hello.pex: hello.srf             ppack -e hello.srf -ohello.pex         install: hello.pex         ←これを追加             isd =w hello.pex      ←これを追加 「make install」とタイプすれば、「hello.pex」の作成・転送までが自動的に行われます。 hello.pexから利用するデータファイル等がある場合は、データファイルの転送も追加しておきましょう。         all: hello.srf         pex: hello.pex         hello.srf: hello.c             pcc33 hello.c         hello.pex: hello.srf             ppack -e hello.srf -ohello.pex         install: hello.pex             isd =w hello.pex             isd =w hell_dat.fpk    ←これを追加 ------------------------------------------------------------------------------------------------------------------------------- ちょっと長くなったので、今回はここまでです。 残り3つのファイルシステムコマンドについては、次回へ。 (…続きます) * Fri Dec 20 12:30:00 JST 2002 Naoyuki Sawa - ISDマニュアル#1 P/ECE開発ツールの中でも特に重要なのが、USB転送ツール/簡易モニター『ISD』です。 地味ながら非常に多機能で、PC側のツールとして必要な機能をほとんど全部備えています。 バージョンチェック、ファイル転送、画面キャプチャ、プログラムの一時停止、カーネル更新などが、これ一つでまかなえます。 コマンドラインツールを好む開発者さんは、既に使いこなしておられるのではないでしょうか。 ISDを使う際の問題点として、P/ECE開発環境にはISDの詳しい説明が入っていない、という点が挙げられます。 ISDのソース(C:\usr\PIECE\tools\isd)や、「isd ?」で表示されるヘルプで充分とも言えますが、やはりまとまった説明が欲しいところです。 (僕はたまにしかISDを使わないので、そのたびに使い方を忘れて、ISDのソースを読みかえしたりしてます…(^^;) そこで、今回より数回に分けて、ISDの使い方をまとめてみようと思います。 今回はまず、ISDの起動方法についてです。 ------------------------------------------------------------------------------------------------------------------------------- ======== 起動方法 ======== ISDは、三つの起動モードを持っています。 ・「プログラム実行モード」 ・「引数オペレーションモード」 ・「入力オペレーションモード」 です。 ※「引数オペレーションモード」と「入力オペレーションモード」のネーミングは、ISDのソースのコメントに由来します。  「プログラム実行モード」は便宜上、僕が勝手に名付けました。 -------------------- プログラム実行モード -------------------- 起動方法1:  isd [#<デバイス番号>] -run 起動方法2:  isd [#<デバイス番号>] -run 起動方法3:  isd [#<デバイス番号>] -run srfファイルを直接実行するモードです。 srfファイルの実行開始後、ISDはすぐに終了します。 起動方法1のように、srfファイル名を指定すると、そのsrfファイルを実行します。 例1:     isd -run C:\usr\PIECE\app\hello\hello.srf         「C:\usr\PIECE\app\hello\hello.srf」を実行します。 起動方法2のように、srfファイルの拡張子「.srf」を省略しても構いません。 例2:     isd -run C:\usr\PIECE\app\hello\hello         「C:\usr\PIECE\app\hello\hello.srf」を実行します。 起動方法3のように、ファイル名の指定を完全に省略すると、現在の作業フォルダからsrfファイルを探して、最初に見つかったsrfファイルを実行します。 現在の作業フォルダに複数のsrfファイルがあった場合は、どのsrfファイルが実行されるかは予測できません。 例3:     C:         cd \usr\PIECE\app\hello\         isd -run         「C:\usr\PIECE\app\hello\」フォルダの中にある「hello.srf」を実行します。 P/ECEが複数台接続されているときは、デバイス番号を指定して、二台目以降のP/ECEでsrfファイルを実行することもできます。 デバイス番号は、0が一台目のP/ECE、1が二台目のP/ECE、2が三台目のP/ECE、...を表します。 デバイス番号を指定しなければ、一台目のP/ECEでsrfファイルを実行します。 デバイス番号を指定することは滅多にありませんが、赤外線通信プログラムなどを開発している場合はこの機能が便利に使えます。 Makefileやバッチファイルなどに、         isd #0 -run ir.srf         isd #1 -run ir.srf と書いておくと、「ir.srf」を二台のP/ECEで実行することができます。 「プログラム実行モード」については、以上です。 「プログラム起動モード」を簡単に使うために、バッチファイル「C:\usr\PIECE\bin\run.bat」が用意されています。 「C:\usr\PIECE\bin\run.bat」の内容は、次のとおりです。         @isd -run %1 srfファイルを実行するためによく「run」コマンドを使いますが、「run」コマンドはISDの「プログラム実行モード」の別名なのです。 ちなみに先頭の「@」は、バッチファイルの中から実行するコマンド名を、画面に表示しないようにするための記号です。 だから、「run」コマンドを使っても、バッチファイルの中からISDを呼んでいる様子は画面に表示されません。 ------------------------ 引数オペレーションモード ------------------------ 起動方法1:  isd [#デバイス番号] <コマンド> 起動方法2:  isd [#デバイス番号] <コマンド>;<コマンド>;... ISDへのコマンドを、ISD起動時にいっしょに指定するモードです。 各コマンドを実行後、ISDはすぐに終了します。 なお、デバイス番号については「プログラム実行モード」と同じですので、説明を省略します。 起動方法1は、コマンドを一つだけ指定する、いちばん基本的な方法です。 例1−1:   isd v         P/ECEカーネルのバージョン情報を表示します。         コマンド「v」は、バージョン情報を表示するためのコマンドです。         (※個々のコマンドの詳細については後述します。以下同様) コマンドが空白を含む場合は、コマンド全体を「"」で囲むのが一般的ですが、必須ではありません。 例1−2:   isd "d 100000"         isd d 100000         どちらも同じく、0x100000番地から128バイト分のメモリ内容をバイナリダンプします。 コマンド「d」は、メモリ内容をバイナリダンプするためのコマンドです。 有効なコマンドの後ろに不要な文字列がくっついていても、警告もなく単に無視されますが、このふるまいに依存するのは望ましくありません。 次の例は、間違ったコマンド指定の例です。 例1−3:   isd vHOGE         コマンド「v」の後の「HOGE」は無視されます。         「isd v」と指定したのと同じ結果となり、「HOGE」に対する警告なども表示されません。 一度に複数のコマンドを指定することもできます。 複数のコマンドを指定する場合は、起動方法2のように、各コマンドを「;」で区切って並べます。 例2−1:   isd v;=         バージョン情報と、ファイル一覧を表示します。         コマンド「=」は、ファイル一覧を表示するためのコマンドです。 コマンド並びが空白を含む場合は、コマンド並び全体を「"」で囲むのが一般的ですが、必須ではありません。 例2−2:   isd "d 100000;d 200000"         isd d 100000;d 200000         どちらも同じく、0x100000番地からの128バイト分のメモリ内容をバイナリダンプし、         続けて、0x200000番地からの128バイト分のメモリ内容をバイナリダンプします。 コマンドを区切る「;」の左右には、空白文字を置かないほうがいいでしょう。 実際には、「;」左側には空白を含めてどんな文字があっても無視されるのですが、このふるまいに依存するのは望ましくありません。 「;」の右側に空白文字を置いた場合は、警告が表示されます。 次の例は、間違ったコマンド指定の例です。 例2−3:   isd vHOGE;=         例1−3と同じく、コマンド「v」の後の「HOGE」は無視されます。         「isd v;=」と指定したのと同じ結果となり、「HOGE」に対する警告なども表示されません。 例2−4:   isd "v ; =" isd v ; =         どちらも同じく、「;」の後ろにある空白文字に対して、次のような警告が表示されます。         skip ' '         警告は出るもののコマンド自体は正しく実行され、バージョン情報とファイル一覧が表示されます。         「;」の左側にある空白文字に対しては、警告は表示されません。         空白文字だから無視されたのではなく、「;」の左側にはどんな文字があっても無視されるのです。(空白もHOGEも同じです) ------------------------ 入力オペレーションモード ------------------------ 起動方法:   isd [#<デバイス番号>] ISDへのコマンドを、対話形式で一行づつ指定するモードです。 このモードでISDを起動すると、P/ECEに接続後、プロンプトを表示してコマンド待ち受け状態となります。 コマンド「q」(終了)を指定するまで、ISDは終了しません。 なお、デバイス番号については「プログラム実行モード」と同じですので、説明を省略します。 例1:     isd         v         =         q         一行目:「入力オペレーションモード」でISDを起動します。         二行目:コマンド「v」を指定し、バージョン情報を表示します。         三行目:コマンド「=」を指定し、ファイル一覧を表示します。         四行目:コマンド「q」を指定し、ISDを終了します。 「引数オペレーションモード」と同じく、「入力オペレーションモード」でも一度に複数のコマンドを指定できます。 例2:     isd         v;=         d 100000;d 200000         q         一行目:「入力オペレーションモード」でISDを起動します。         二行目:バージョン情報を表示し、続けてでファイル一覧を表示します。         三行目:0x100000番地からの128バイト分をバイナリダンプし、続けて0x200000番地からの128バイト分をバイナリダンプします。         四行目:コマンド「q」を指定し、ISDを終了します。 「引数オペレーションモード」では、コマンドを「"」で囲ってはいけません。 次の例は、間違ったコマンド指定の例です。 例3:     isd         "v;="         q         二行目先頭の「"」に対する警告が表示され、次に「;」が見つかるまでの全ての文字が無視されます。         従って、コマンド「v」は実行されず、バージョン情報は表示されません。         「;」の後ろにあるコマンド「=」は実行されます。 ------------------------------------------------------------------------------------------------------------------------------- 次回は、各コマンドの詳細について見ていこうと思います。 (続きます…) * Thu Dec 12 06:30:00 JST 2002 Naoyuki Sawa - strtok()に大問題 S1C33 Family Cコンパイラパッケージ(P/ECEの標準Cライブラリ)のstrtok()関数には、重大な問題があります。 指定した文字列の終端を突き抜けてトークン分割を続けてしまう、というものです。 --------------------------------------------------------------------------------------------------- まず、PC上のCコンパイラを使って、次のプログラムを実行してみましょう。 #include #include #include char s1[] = "a/b/c\0d/e/f"; int main() {     char* p;     p = strtok(s1, "/");     while(p != NULL) {         fprintf(stderr, "(%s)", p);         p = strtok(NULL, "/");     }     fprintf(stderr, "\n");     return 0; } 文字列s1の中の「/」で区切られた各部分を、順に「(」〜「)」で囲って表示するプログラムです。 文字列s1は「a/b/c\0d/e/f」ですが、「c」の後ろのヌル文字で終端しているので、「a/b/c」と等価です。 従って、結果は次のようになります。 「(a)(b)(c)」 --------------------------------------------------------------------------------------------------- では次に、P/ECEで次のプログラムを実行してみましょう。 ソースのダウンロードはこちら: http://www.piece-me.org/archive/strtok1-20021212-src.zip #include #include #include #include unsigned char vbuff[DISP_X * DISP_Y]; /* 仮想VRAM */ char s1[] = "a/b/c\0d/e/f"; void pceAppInit() {     char* p;     /* 一般的な初期化 */     pceLCDDispStop();     pceLCDSetBuffer(vbuff);     pceLCDDispStart(); /* 画面クリア */     memset(vbuff, 0, sizeof vbuff);     pceFontSetPos(0, 0);     /*{{この部分が実験コードの本体です*/     p = strtok(s1, "/");     while(p != NULL) {         pceFontPrintf("(%s)", p);         p = strtok(NULL, "/");     }     /*}}この部分が実験コードの本体です*/     /* 画面転送 */     pceLCDTrans(); } void pceAppProc(int count) { /* SELECTボタンが押されたら終了します */ if(pcePadGet() & TRG_SELECT) pceAppReqExit(0); } void pceAppExit() { } 実験内容はPC用のプログラムと同じですので、「(a)(b)(c)」と表示されるはずですが、実際には、 「(a)(b)(c)(d)(e)(f)」 と表示されてしまいます! --------------------------------------------------------------------------------------------------- S1C33 Family Cコンパイラパッケージのstrtok()関数は、文字列終端のヌル文字の検出に問題があります。 どうやら、ヌル文字が二つ以上連続していた場合にのみ、文字列終端を認識するようです。 例えば、先ほどのプログラムで、 char s1[] = "a/b/c\0d/e/f"; を、 char s1[] = "a/b/c\0\0d/e/f"; に変更すると、 「(a)(b)(c)」 と表示されます。 先ほどのプログラムで、表示が「(f)」までで終わっていたのも、たまたま「f」の後ろにヌル文字がいくつか 並んでいたためで、もしそうなっていなければ永遠に終わらず、 「(a)(b)(c)(d)(e)(f)(ゴミ)(ゴミ)(ゴミ)...」 と表示されてしまっていたでしょう。 --------------------------------------------------------------------------------------------------- 結論: S1C33 Family Cコンパイラパッケージのstrtok()関数は、使ってはいけません。 回避方法: アプリケーションプログラムの中でstrtok()を実装して、ライブラリ内のstrtok()を置き換えてしまいましょう。 char* strtok(char* s1, const char* s2) { /* find next token in s1[] delimited by s2[] */     static char* ssave = ""; /* for safety */     char *sbegin, *send;     sbegin = s1 ? s1 : ssave;     sbegin += strspn(sbegin, s2);     if(*sbegin == '\0') { /* end of scan */         ssave = ""; /* for safety */         return NULL;     }     send = sbegin + strcspn(sbegin, s2);     if(*send != '\0') *send++ = '\0';     ssave = send;     return sbegin; } // P.J.プラウガー著「標準Cライブラリ ANSI/ISO/JIS C規格」(トッパン) // 初版 467ページ 図14.20 strtok.c をそっくりそのまま使わせて頂きました。 そういえば、おでマルのソースも標準のstrtok()を使わず、自前のstrtok()関数を用意してそれを使っていました。 (\usr\PIECE\app\odemaru\piece_ex.c 56行目〜 PIECE_StrTok()関数) 標準strtok()関数の問題を回避するためだったのですね。 * Sun Oct 13 10:03:00 JST 2002 Naoyuki Sawa - 64ビット整数演算ライブラリ C言語で64ビット整数演算を行うには「long long」を使います。符号無しの場合は「unsigned long long」です。 「long long」型は、1999年に取り決められた新しいC言語規格に含まれている仕様です。 P/ECEのCコンパイラは古いC言語規格に沿って作られていますが、「long long」型は既にサポートされています。 それでは、64ビット整数演算を試してみましょう。次のようなプログラムを書いて: void smain() { long long a, b, c; a = 15372468987LL; b = 12345678901LL; c = a + b; } コンパイルすると... test1.o: Warning: Unresolved external symbol '__adddi3'. あれ?リンクエラーになってしまいました。__adddi3という関数が定義されていない、と言っています。 そんな関数は呼んだ覚えがないのですが、これはいったいどういうことでしょうか? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - P/ECEのCPU S1C33209は、64ビット整数演算を行うためのハードウェアを持っていません。 64ビット整数演算は直接アセンブラコードに変換されるのではなく、64ビット整数演算を行う関数の呼び出しに置き換えられます。 例えば、先ほどのプログラムの「c = a + b」の部分は、次のようなアセンブラコードにコンパイルされます。 xld.w %r12,[%sp+12] /* (%r13,%r12) に足される数(a)を入れます。 */ xld.w %r13,[%sp+16] xld.w %r14,[%sp+20] /* (%r15,%r14) に足す数(b)を入れます。 */ xld.w %r15,[%sp+24] xcall __adddi3 /* (%r11,%r10) = (%r13,%r12) + (%r15,%r14) を行うサブルーチン */ xld.w [%sp+28],%r10 /* 結果をメモリに書き戻します。 */ xld.w [%sp+32],%r11 ちなみに、関数呼び出しに置き換えられるのは64ビット整数演算に限ったことではなく、 浮動小数点演算や32ビット除算なども、実は関数呼び出しに置き換えられているのです。 32ビット除算に関しては、CPUが除算命令を持っていますが、35命令も必要とします。 あちこちで32ビット除算を行う場合、その場その場に展開していてはプログラムサイズが非常に大きくなってしまうので、 プログラムサイズを小さくするために、まとめて関数呼び出しにしているのですね。 浮動小数点演算を行う関数(__addsf3や__divsf3など)は、浮動小数点演算ライブラリとして、\usr\PIECE\lib\fp.lib に入っています。 32ビット除算を行う関数(__divsi3や__modsi3など)は、整数剰余演算ライブラリとして、\usr\PIECE\lib\idiv.lib に入っています。 このあたりの説明は、 『S1C33 Family Cコンパイラパッケージ マニュアル』(\usr\PIECE\docs\datasheet\EPSON\S5U1C33000C_J.pdf) 7.エミュレーションライブラリ(93〜95ページ) に載っていますので、ご参照ください。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - それでは、64ビット整数演算を行う関数は、どのライブラリに入っているのでしょうか? ・・・入っていません! 入っていないのです。 Cコンパイラは確かに「long long」を認識し、64ビット整数演算関数を呼び出すアセンブラコードを生成しているのですが、 肝心の64ビット整数演算ライブラリが提供されていません。 P/ECEのCD-ROMに入れ忘れた、というわけではなさそうです。 先ほどの資料でも、64ビット整数演算ライブラリについては全く触れられていませんから、 どうやら、EPSONさんのCコンパイラパッケージ配布セットにもともと含まれていないようです。 Cコンパイラは対応しているのに、ライブラリが提供されていない、というのはどういうことでしょうか? 考えられるのは、Cコンパイラの「long long」対応が不充分で、ライブラリを用意しただけでは正しく使えないため、 64ビット整数演算ライブラリも作ったけど配布しなかった、というケースです。 しかしながら、いくつかプログラムを書いて試してみた限りでは、生成されるコードに特に問題があるようには見えませんでした。 そこで、64ビット整数演算ライブラリを自前で用意してみることにしました。 64ビット整数演算の関数名は次のとおりです: __negdi2 (%r11,%r10) ← -(%r13,%r12) __adddi3 (%r11,%r10) ← (%r13,%r12) + (%r15,%r14) __subdi3 (%r11,%r10) ← (%r13,%r12) - (%r15,%r14) __muldi3 (%r11,%r10) ← (%r13,%r12) * (%r15,%r14) __divdi3 (%r11,%r10) ← (%r13,%r12) / (%r15,%r14) ※符号付き __udivdi3 (%r11,%r10) ← (%r13,%r12) / (%r15,%r14) ※符号無し __moddi3 (%r11,%r10) ← (%r13,%r12) % (%r15,%r14) ※符号付き __umoddi3 (%r11,%r10) ← (%r13,%r12) % (%r15,%r14) ※符号無し これらの関数を実装したソースはこちら。テスト用プログラムも含んでいます: http://www.piece-me.org/archive/int64-20021013-src.zip テストプログラムの結果の画面はこうなります: http://www.piece-me.org/int64-20021013.png とりあえず、ちゃんと動いているみたいですね。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 今回実装した64ビット整数演算ライブラリには、まだ次のような問題点・不安点があります。 一部未実装の関数があります -------------------------- 浮動小数点⇔64ビット整数の変換を行うための関数をまだ実装していません。 浮動小数点⇔64ビット整数の変換に必要な関数は、__fixsfdi,__fixunssfdi,__floatdisfです。 浮動小数点が絡んでくるとちょっと面倒なのと、このような型変換は当面必要ないと考えたので、今回は実装しませんでした。 また、なぜか浮動小数点⇔64ビット整数の変換時に__cmpdi2という関数が呼ばれることもあります。 関数名から推測すると「long long」同士の比較のようですが、 通常の「long long」同士の比較は関数呼び出しに置き換えられず、直接アセンブラコードに展開されているのです。 例えば、次のようなプログラムは: void test(long long a, long long b) { if(a > b) return; ... } 直接アセンブラコードに展開されます。__cmpdi2は呼ばれていません。 cmp %r13,%r15 /* 上位32ビットの比較 */ xjrgt __L1 xjrne __L2 cmp %r12,%r14 /* 下位32ビットの比較 */ xjrugt __L1 __L2: ... __L1: ret 予想ですが、たぶん昔は通常の「long long」同士の比較も__cmpdi2の呼び出しに変換されていたのだと思います。 その後、Cコンパイラの最適化により、効率の良いコードが生成できるようになって、 通常の比較は関数呼び出しではなく、直接アセンブラコードに展開されるようになったのではないでしょうか。 浮動小数点⇔64ビット整数の中の比較処理(なぜ型変換に比較が必要なのかよくわかっていないのですが(^^;)は、 何らかの理由により(もしかしたら最適化忘れ?)昔の名残で__cmpdi2の呼び出しのままになっているのではないかと思います。 %r4と%r12〜%r15が破壊されます ----------------------------- S1C33 Family Cコンパイラの関数呼び出し規約により、関数は%r4〜%7・%r12〜%r15を破壊してもいいことになっています。 今回実装した64ビット整数演算ライブラリも、これらのレジスタを作業用に使うので、呼び出されたときの値を保持しません。 たぶんこれで問題ないとは思うのですが、浮動小数点演算ライブラリや整数剰余演算ライブラリ、64ビット整数演算ライブラリは Cコンパイラそのものに深く関わっていますので、もしかしたらCコンパイラはこれらのライブラリ関数に対して、 通常の関数とは異なる、特別な関数呼び出し規約を期待しているかもしれません。 もしもCコンパイラがこれらの関数に対して“全レジスタが保存されること”を期待していた場合、 予期しないレジスタが破壊されてしまうことによって、間違った動作を引き起こす可能性があります。 これまで実験したところ問題ないので、たぶん大丈夫だとは思うのですが、『S1C33 Family Cコンパイラパッケージ マニュアル』に この点に関する説明がないため、まだ少し不安です。 乗算・除算は遅いです -------------------- 乗算ルーチン(__muldi3)は、古典的な1ビットづつの筆算方式を採っています。 S1C33 CPUの乗算命令を利用して、32ビットづつまとめて計算すれば、もっと高速化できると思います。 除算・剰余ルーチン(__divdi3,__moddi3など)は、S1C33 CPUのdiv0s,div1,div2s,div3s命令の動作をそっくりそのまま真似て作りました。 これらの命令が32ビットで処理しているところを、単に64ビット分行うように拡張しただけです。 div1命令などがハードウェアで行っている処理をソフトウェアに置き換えましたので、1ビット当たり10倍以上遅くなっています。 さらにビット数が2倍ですので、20倍以上。他の処理も考えると、30倍以上は遅くなっていると思います。 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - バグ・高速化方法など、お気付きの点がありましたら、ご一報くだされば幸いです。 64ビット整数演算が実際にどの程度必要なのか?と考えると、あまり力を入れる部分ではないような気もしますが、 実用性はともかく、小さなサブルーチンのアセンブラコーディングは楽しいですよね。 * Thu Sep 19 12:30:00 JST 2002 Naoyuki Sawa - calloc()が失敗する ---- 状況 ---- 標準Cライブラリには、主に二種類のメモリ割り当て関数があります。 malloc()とcalloc()です。 malloc()が単にメモリを割り当てるだけなのに対し、calloc()は割り当てたメモリのゼロクリアも行ってくれます。 プログラム側でゼロクリアを行わなくて済む分calloc()の方が便利なので、malloc()よりもcalloc()の方を好んで使っていたのですが、 P/ECE開発環境のcalloc()(以下、P/ECE版calloc()と呼ぶことにします)にはちょっと落とし穴があることに気付きました。 同じサイズのメモリ割り当てで、malloc()を使えば成功するのにcalloc()を使うと失敗することがある、というものです。 サンプルプログラムを用意しました。 http://www.piece-me.org/archive/calloc-20020919-src.zip このサンプルプログラムは、16キロバイトのヒープ領域から、五種類の方法で8キロバイトのメモリ割り当てを試行します。 五種類の方法は、次のようなものです。 1. calloc(2048, 4) 4バイト×2048個のメモリ割り当て 2. calloc(4, 2048) 2048バイト×4個のメモリ割り当て 3. calloc(8192, 1) 1バイト×8192個のメモリ割り当て 4. calloc(1, 8192) 8192バイト×1個のメモリ割り当て 5. malloc(8192) 8192バイトのメモリ割り当て いずれも結果的には8キロバイトのメモリ割り当てが行われるはずなので、すべて成功するはずです。 ところが、サンプルプログラムを実行してみると、 3. calloc(8192, 1) 1バイト×8192個のメモリ割り当て が失敗するのです。 それ以外の1,2,4,5は成功します。なぜでしょうか? ---- 原因 ---- calloc()の引数仕様は、次のとおりです。 void* calloc(size_t num, size_t size); num 要素数 size 各要素のバイト単位での長さ 一般的な処理系のcalloc()の実装は、だいたい次のようになっています。 void* calloc(size_t num, size_t size) { int n; void* p; n = num * size; /* 割り当てバイト数を計算 */ p = malloc(n); /* 割り当てを行ってみる */ if(p != NULL) memset(p, 0, n); /* 成功ならゼロクリア */ return p; } ところが、P/ECE版calloc()の実装は、次のようになっているみたいです。 void* calloc(size_t num, size_t size) { int n; void* p; size = size + 3 & ~3; /* 要素サイズを4バイト単位に切り上げ */ n = num * size; /* 割り当てバイト数を計算 */ p = malloc(n); /* 割り当てを行ってみる */ if(p != NULL) memset(p, 0, n); /* 成功ならゼロクリア */ return p; } 割り当てバイト数を計算する前に、要素サイズを4バイト単位に切り上げています。 これを踏まえて、先ほどの五種類の割り当て方法でそれぞれ何バイトの割り当てが行われようとしていたのかを考えてみると、 1. calloc(2048, 4) 4バイト×2048個のメモリ割り当て → (4+3&~3) * 2048 = 4 * 2048 = 8192[バイト] 2. calloc(4, 2048) 2048バイト×4個のメモリ割り当て → (2048+3&~3) * 4 = 2048 * 4 = 8192[バイト] 3. calloc(8192, 1) 1バイト×8192個のメモリ割り当て → (1+3&~3) * 8192 = 4 * 8192 = 32768[バイト] 4. calloc(1, 8192) 8192バイト×1個のメモリ割り当て → (8192+3&~1) * 1 = 8192 * 1 = 8192[バイト] 5. malloc(8192) 8192バイトのメモリ割り当て → 8192[バイト] 3だけが32KBもの割り当てを行おうとしています。 ヒープ領域は16KBしか準備していませんから、なるほど失敗するはずです。 ヒープ領域が32KB以上あれば成功しますが、24KB分もの領域が無駄になってしまいます。 ではなぜP/ECE版calloc()は、わざわざ要素サイズを4バイト単位に切り上げているのでしょうか? 確かにcalloc()の仕様では、割り当てられた各要素が適切なバイト境界に配置されるよう、引数sizeによって割り当てサイズを調整しても構わないことになっています。 たぶん、int,short,char型のメンバ変数が混在している構造体のための配慮だと思います。 struct TEST { int a; /* +0〜3 */ char b; /* +4 */ }; /* =5バイト */ 構造体TESTのサイズは5バイトなので(※実はそうではありません。後述します)、すき間なく並べると、メンバ変数aは5バイトごとに配置されてしまいます。 S1C33 CPUでは、int変数は4バイト境界に配置されていなければいけません。 二つ目以降の要素のメンバ変数aは4バイト境界に配置されていないので、読み書きしようとするとアドレスエラーが発生してしまいます。 struct TEST +=========+ +0 | | | int a | | | | | +---------+ +4 | char b | +=========+ +5 <- 二つ目の要素のメンバ変数aにアクセスするとアドレスエラー! | | | int a | | | | | +---------+ +9 | char b | +=========+ +10 : : アドレスエラーを防ぐには、メンバ変数bの後ろに3バイト分のパディング(詰め物)を追加して、構造体TESTのサイズを4の倍数にする必要があります。 struct TEST +=========+ +0 | | | int a | | | | | +---------+ +4 | char b | +---------+ +5 | | | 詰め物 | | | +=========+ +8 <- OK! | | | int a | | | | | +---------+ +12 | char b | +---------+ +13 | | | 詰め物 | | | +=========+ +16 : : P/ECE版calloc()は、このパディングの分のサイズを考慮して、要素サイズを4バイト単位に切り上げているのだと思います。 しかしながら実際には、このような配慮は不要です。 構造体の各メンバが適切なバイト境界に配置されるようにパディングを追加するのは、ライブラリ関数の役割ではなくCコンパイラの役割だからです。 先ほどの構造体TESTのサイズを調べてみると、 sizeof(struct TEST) → 8 8バイトになっています。 構造体TESTの配列を作成した場合に、二つ目以降の要素のメンバ変数aが4バイト境界に配置されるよう、最後に3バイトのパデイングが追加されたからです。 単に次のように書くだけで大丈夫です。 calloc(num, sizeof(struct TEST)); ←同じ→ calloc(num, 8); というわけで、P/ECE版calloc()の実装は明らかに間違いというわけではありません。 適切なバイト境界への配置を心配しすぎたあまり、割り当てサイズが大きくなりすぎてしまっただけです。 しかし、1バイト単位のメモリ割り当て(いちばんよく使うパターンです)が常に3倍もの無駄な領域を伴うというのは、ちょっとキビシイところです。 calloc(1000, sizeof(char)) /* 1000文字分のメモリ割り当て。しかし実際には4000文字分!3000文字分は無駄 */ ---- 対策 ---- アプリケーションプログラムの中でcalloc()を実装して、ライブラリ内のcalloc()を置き換えてしまいましょう。 void* calloc(size_t num, size_t size) { int n; void* p; n = num * size; /* 割り当てバイト数を計算 */ p = malloc(n); /* 割り当てを行ってみる */ if(p != NULL) memset(p, 0, n); /* 成功ならゼロクリア */ return p; } * Fri Aug 15 12:30:00 JST 2002 Naoyuki Sawa - Cコンパイラの最適化ミス・レポート P/ECE開発環境のCコンパイラの、最適化ミスによる不具合レポートです。 まず、僕が作ろうとした関数「hit_test()」について説明します。 この関数の目的は、盤面上に4×4のコマが置けるかどうか?を調べることです。 コマを置きたい位置の左上座標を指定して、置けるなら0、置けなければ1を返します。 置けない理由としては、次のようなものがあります。 ・指定された座標を左上原点とする4×4の範囲が、盤面をはみ出している場合。 ・指定された座標を左上原点とする4×4の範囲に、既にコマが置かれている場合。 この関数を使って不具合を再現する、サンプルプログラムを示します。 (プロジェクト一式はこちら: http://www.piece-me.org/archive/forbug-20020815-src.zip) --------------------------------------------------------------------------------------------------- C01| /* C02| * forbug.c C03| * C04| * for()文の最適化バグ再現プログラム C05| * Copyright (C) 2002 Naoyuki Sawa C06| * C07| * * Thu Aug 15 04:30:00 JST 2002 Naoyuki Sawa C08| * - 作成開始。 C09| */ C10| #include C11| C12| /* 最適化コンパイル(問題発生する): C13| * pcc33 -b -gp=0x0 -near -O2 -Wall forbug.c pat.c C14| * 最適化しないコンパイル(問題発生しない): C15| * pcc33 -b -gp=0x0 -near -Wall forbug.c pat.c C16| */ C17| C18| /* 16x16マスの盤面定義 C19| * 各位置の要素が、0ならコマが置かれていません。 C20| * 1ならコマが置かれています。 C21| */ C22| #define COLS 16 C23| #define ROWS 16 C24| unsigned char field[COLS][ROWS]; C25| C26| /* (x,y)〜(x+3,y+3)の4x4マスの範囲に、既にコマが置かれているかを調べます。 C27| * * (x,y)〜(x+3,y+3)の一部でも盤面をはみ出していたら、1を返します。 C28| * (x,y)〜(x+3,y+3)の範囲にひとつでもコマが置かれていたら、1を返します。 C29| * * (x,y)〜(x+3,y+3)が盤面内で、その範囲にひとつもコマがない場合のみ、0を返します。 C30| */ C31| int C32| hit_test(int x, int y) C33| { C34| int x0, y0; /* 縦横のループカウンタ */ C35| int x1, y1; /* 盤面を走査する座標変数 */ C36| C37| for(y0 = 0, y1 = y; y0 < 4; y0++, y1++) { C38| if(y1 < 0 || ROWS - 1 < y1) { /* 縦はみ出しチェック */ C39| return 1; /* はみ出してたら1を返す */ C40| } C41| for(x0 = 0, x1 = x; x0 < 4; x0++, x1++) { C42| if(x1 < 0 || COLS - 1 < x1) { /* 横はみ出しチェック */ C43| return 1; /* はみ出してたら1を返す */ C44| } C45| if(field[y1][x1]) { /* 既にコマが置かれてるかチェック */ C46| return 1; /* 置かれてたら1を返す */ C47| } C48| } C49| } C50| C51| return 0; C52| } C53| C54| void C55| smain() C56| { C57| int retval; C58| C59| /* (-1,8)〜(2,11)の4x4の範囲が盤面内で、ひとつもコマが置かれていないか? C60| * 明らかに盤面外(左にはみ出している)なので、1が帰るはずなのですが、 C61| * 最適化コンパイルすると0が帰ってきます! C62| */ C63| retval = hit_test(-1, 8); C64| cls(); C65| printnum(retval); C66| while(!pad()) { } C67| } --------------------------------------------------------------------------------------------------- hit_test()関数は、特に複雑な処理を行っているわけではありません。 ゲームに限らない、ごく一般的な重なり判定のロジックだと思います。 ところが、このソースを「-O2」オプション付きで最適化コンパイルすると、正しい結果が帰ってきません。 サンプルプログラムでは、(-1,8)を基準とする4×4の範囲にブロックが置けるかどうかを判定しています。 明らかに左側にはみ出ていますので、C42行目の判定に引っかかり、「コマが置けない=1」が帰ってくるはずです。 しかし実際に試してみると、「コマが置ける=0」が帰ってくるのです。 試しに「-O2」オプション無しで最適化しないコンパイルを行ってみると、正しく1が帰ってきます。 ということは、どうやらCコンパイラの最適化ミスのようです。 そこで、上記hit_test()関数を最適化コンパイルした場合の、アセンブラソースを調べてみました。 --------------------------------------------------------------------------------------------------- A01| /* int hit_test(int x, int y) A02| * [in] A03| * r12 x A04| * r13 y A05| * [out] A06| * r10 result A07| */ A08| .align 1 A09| .global hit_test A10| hit_test: A11| ; .frame %sp,4,$31 # vars= 0, regs= 1/0, args= 0, extra= 0 A12| ; .mask 0x80000000,-4 A13| ; .fmask 0x00000000,0 A14| ld.w %r5,0x0 /* r5 <- 0 (r5が変数y0に対応) */ A15| xld.w %r11,field /* r11 <- &field[0][0] */ A16| ld.w %r10,%r13 /* r10 <- y */ A17| xsll %r10,4 /* r10 <- y * 16 */ A18| add %r10,%r11 /* r10 <- &field[y][0] */ A19| __L5: A20| xcmp %r13,15 /* IF y > 15 THEN L16 */ A21| xjrugt __L16 /* (符号無し比較なので、y<0の場合もこれで判定できます) */ A22| ld.w %r14,0x0 /* r14 <- 0 (r14が変数x0に対応) */ A23| ld.w %r11,%r12 /* r11 <- x */ A24| add %r11,%r10 /* r11 <- &field[y][ x] */ A25| xadd %r4,%r10,15 /* r4 <- &field[y][15] */ A26| __L10: A27| cmp %r11,%r4 /* IF &field[y][x] > &field[y][15] THEN L16 */ A28| xjrugt __L16 /* (※ここが問題!これではx<0の場合を判定できません) */ A29| xld.ub %r15,[%r11] /* r15 <- field[y][x] */ A30| cmp %r15,0x0 /* IF field[y][x] = 0 THEN L9 */ A31| xjreq __L9 A32| __L16: A33| xld.w %r10,0x00000001 /* return 1 */ A34| xjp __L15 A35| __L9: A36| xadd %r14,%r14,1 /* r14 <- r14 + 1 (r14が変数x0に対応) */ A37| xadd %r11,%r11,1 /* r11 <- &field[y][x+1,2,3] (ポインタを右隣へ) */ A38| xcmp %r14,3 /* IF x0 <= 3 THEN L10 */ A39| xjrle __L10 A40| xadd %r5,%r5,1 /* r5 <- r5 + 1 (r5が変数y0に対応) */ A41| xadd %r10,%r10,16 /* r10 <- &field[y+1,2,3][0] (ポインタを下隣へ) */ A42| xadd %r13,%r13,1 /* y <- y + 1 */ A43| xcmp %r5,3 /* IF y0 <= 3 THEN L5 */ A44| xjrle __L5 A45| ld.w %r10,%r15 /* return 0 (r15は必ず0になっています。A30〜A31行参照) */ A46| __L15: A47| ret --------------------------------------------------------------------------------------------------- まず注目していただきたい点は、元のCソースの縦方向はみ出し判定ロジック: C38| if(y1 < 0 || ROWS - 1 < y1) { /* 縦はみ出しチェック */ が、次のように最適化されている点です。 A20| xcmp %r13,15 /* IF y > 15 THEN L16 */ A21| xjrugt __L16 /* (符号無し比較なので、y<0の場合もこれで判定できます) */ これは問題ありません。 if((int)y1 < 0 || 15 < (int)y1) { ... } /* 元のCソースのロジック */ と、 if(15 < (unsigned)y1) { ... } /* 最適化されたアセンブラソースのロジック */ は、等価だからです。 例えば、「y1 = -1」の場合、元のCソースのロジックでは「-1 < 0」で条件が成立します。 -1は符号無し整数と見なせば0xffffffffなので、アセンブラのロジックでも「15 < 0xffffffff」で成立します。 というわけで、縦方向のはみ出し判定には問題ありません。 問題は、横方向のはみ出し判定です。 元のCソースの横方向はみ出し判定ロジック: C42| if(x1 < 0 || COLS - 1 < x1) { /* 横はみ出しチェック */ が、次のように最適化されている点に注目してください。 A27| cmp %r11,%r4 /* IF &field[y][x] > &field[y][15] THEN L16 */ A28| xjrugt __L16 /* (※ここが問題!これではx<0の場合を判定できません) */ これはダメです! X座標そのままでの比較ではなく、盤面上へのポインタにした後で、アドレス値を比較するように最適化されています。 アドレス値で比較するのは構わないのですが、アドレス比較と先ほどの符号無し比較による最適化の方法は併用できません。 例えば「&field[0][0] = 0x110000, x = -1, y = 8」の場合、最適化後のロジックでは次のような判定を行ってしまいます。 IF &field[8][-1] > &field[8][15] THEN L16 アドレス値を展開すると、 IF 0x11007f > 0x11008f THEN L16 本当はL16へ分岐しなければいけないのですが、このロジックでは左側へのはみ出しが判定できません。 だからサンプルプログラムの結果が、「コマが置けない=1」ではなく、「コマが置ける=0」になってしまうのです。 コンパイラの最適化ミスというのは、P/ECEのCコンパイラに限らず、よくあることです。 最適化ミスが発生する状況と、回避方法がきちんとわかっていれば、重大な問題にはなりません。 が、今回のケースでちょっとキビシイなあ...と感じるのは、 あまりにも普通のケースで問題が生じているため、どういう状況で最適化ミスが発生するのか推測できない点。 また、問題を発生するコードがそもそも単純ですので、別の方法に置き換えても確実に回避できそうにない点です。 for()のかっこの中に「,」を使ってマルチステートメントを書くのがマズイのかと考えて、次のように書き換えてみました。 --------------------------------------------------------------------------------------------------- /* こう書き換えてもダメな例! */ int hit_test(int x, int y) { int x0, y0; /* 縦横のループカウンタ */ int x1, y1; /* 盤面を走査する座標変数 */ y1 = y; for(y0 = 0; y0 < 4; y0++) { if(y1 < 0 || ROWS - 1 < y1) { /* 縦はみ出しチェック */ return 1; /* はみ出してたら1を返す */ } x1 = x; for(x0 = 0; x0 < 4; x0++) { if(x1 < 0 || COLS - 1 < x1) { /* 横はみ出しチェック */ return 1; /* はみ出してたら1を返す */ } if(field[y1][x1]) { /* 既にコマが置かれてるかチェック */ return 1; /* 置かれてたら1を返す */ } x1++; } y1++; } return 0; } --------------------------------------------------------------------------------------------------- が、結果は同じ。 やっぱり左方向のはみ出し判定に失敗し、「コマが置ける=0」が帰ってきます。 いろいろ試した結果、最適化ミスに失敗しない書き方の一例として、次のようなCソースならOKみたいです: --------------------------------------------------------------------------------------------------- /* 一応、こう書き換えればOKみたい... */ int hit_test(int x, int y) { #define x1 (x + x0) #define y1 (y + y0) int x0, y0; /* 縦横のループカウンタ */ for(y0 = 0; y0 < 4; y0++) { if(y1 < 0 || ROWS - 1 < y1) { /* 縦はみ出しチェック */ return 1; /* はみ出してたら1を返す */ } for(x0 = 0; x0 < 4; x0++) { if(x1 < 0 || COLS - 1 < x1) { /* 横はみ出しチェック */ return 1; /* はみ出してたら1を返す */ } if(field[y1][x1]) { /* 既にコマが置かれてるかチェック */ return 1; /* 置かれてたら1を返す */ } } } return 0; #undef x1 #undef y1 } --------------------------------------------------------------------------------------------------- 一応、正しい結果「コマが置けない=1」が帰ってきます。 しかしこれは、hit_test()関数の場合はたまたま最適化ミスに引っかからなくなったというだけで、 根本的な解決にはなっていません。 この不具合の確実な回避方法をご存知のかたがおられましたら、ぜひ教えてください。 よろしくお願いいたします。 さて、今回のレポートで言いたいのは、「P/ECEのCコンパイラはダメ」ということではありません。 前述の通り、どんなコンパイラにも多かれ少なかれ最適化ミスはあると思います。 P/ECEの場合に問題なのは、現時点ではCコンパイラの選択肢がなく、付属のCコンパイラを使うしかないことです。 また、Cコンパイラに不具合があっても、バージョンアップで修正される見込みがほとんどないことです。 もしかしたらEPSONさんは新しいバージョンのCコンパイラを配布しているのかも知れませんが、 エンドユーザーがCコンパイラだけを入手するのはかなり困難だと思われます。 なければ作るしかありません。 新しいgccをS1C33 CPU用に移植…気が遠くなりそうですが、実際に他のCPU用に移植なさっているユーザーさんも 大勢いらっしゃいますので、不可能ではないはず。 また壮大な目標ができてしまいました。 組み込みCPUの勉強・USBの勉強・電子回路の勉強の後はコンパイラの移植…やること目白押しです(^^; * Fri Jul 19 05:00:00 JST 2002 Naoyuki Sawa - コピーするのは instdef.c だけ 自作プログラムに音楽ライブラリを組み込む場合、P/ECE開発環境のフォルダからプログラム作成用のフォルダへ、 いくつかのファイルをコピーしなければいけません。 おさんぽ綾香のチュートリアルでは、次のように指示されています。 綾香の画像ファイルがあるフォルダ(標準ではc:\usr\piece\docs\tutorial\ayaka)に「wavetbl」という フォルダがありますので、その中身の「wavetbl.c」と「sndフォルダ」を貴方の作業フォルダ(ayaka)に コピーしてください。 (C:\usr\PIECE\HTML\tutorial_f.htm 【綾香が歩くプログラムに音楽を追加する】 より) また、P/ECE Hand Bookでは、次のように指示されています。 sysdev\music\makefile sysdev\music\wavetable.c sysdev\music\instdef.c sysdev\music\musdef.h sysdev\music\wave\*.* (P/ECE Hand Book 『開発Tips』 音楽を演奏したい (p.098) より) 僕は記憶力がよくないので、二つ以上のファイル名は覚えてられません(^^; 音楽ライブラリを使った練習プログラムを作るたびに、これらの資料を読み返してコピーするファイルを調べていました。 毎回同じファイルをコピーしなければいけないのなら、なぜライブラリに入れておいてくれなかったのでしょうか? ・・・実は入っていました。 自作プログラムに音楽ライブラリを組み込ために、P/ECE開発環境からコピーする必要があるファイルは、 sysdev\music\instdef.c これひとつだけです。 instdef.cだけをコピーしたサンプルプログラム、ソースはこちら: http://www.piece-me.org/archive/musprg1-20020719-src.zip instdef.cの中には、音色テーブルだけが定義されています。 INST *inst[] = { ... }; 音色テーブルは小さいので、instdef.cファイル自体をコピーしなくても、アプリケーションプログラムのソースの中に instdef.cの内容だけをカット&ペーストしてしまっても良いと思います。 音色データ本体はmuslib.libの中にあり、音色テーブルから参照されている音色データだけが、実際にリンクされます。 さて、instdef.cの音色テーブルは全ての音色への参照を含んでいますので、instdef.cの内容をそのまま使うと、 muslib.libの中にある全ての音色データがリンクされてしまいます。 アプリケーションの曲データが使っていない音色データをリンクすると、プログラムが無駄に大きくなってしまいます。 不要な音色を削除してプログラムを小さくする方法が、"P/ECE" Official WebPage 開発者掲示板 に載っています。 参照記事はこちら: http://www.piece-me.com/kyview/article/d/devpiece/8/opcilm/index.html 音色テーブルを編集して、不要な音色への参照を削除してしまいましょう。 不要な音色をリンクしないサンプルプログラム、ソースはこちら: http://www.piece-me.org/archive/musprg2-20020719-src.zip 先ほどのプログラムはSRFファイルサイズが48KBぐらいありましたが、音色を減らすと8KB程度に小さくなりました。 * Mon Jul 15 21:23:00 JST 2002 Naoyuki Sawa - 1.25の1乗はゼロ? "P/ECE" Official WebPage 開発者掲示板にて質問させて頂いた件の備忘録です。元記事はこちら。 http://www.piece-me.com/kyview/article/d/devpiece/10/kaxilm/index.html ご回答くださった、16びっとさん(http://www.interq.or.jp/www-user/wanderer/)に感謝いたします。 「S1C33 Family Cコンパイラパッケージ」のpow()関数は、ちょっと不具合があるようです。 特定の状況で、ぜんぜん間違った値(ゼロ)を返すことがあります。 微妙なコーディングの違いで、症状が発生したりしなかったりするようで、原因不明です。 再現プログラムはこちら: http://www.piece-me.org/archive/powtest-src.zip "P/ECE" Official WebPage 開発者掲示板にてアドバイス頂いた回避方法は次のとおりです。 pow()を直接呼ぶのではなく、pow()を呼び出すだけの単純な関数pow_wrap()を経由させます。 つまり、 fn() { ... c = pow(a, b); ... } の部分を、 double pow_wrap(double x, double y) { return pow(x, y); } fn() { ... c = pow_wrap(a, b); ... } に変更するのです。なぜこれで治るのかさっぱりわかりませんが…(^^; 回避方法サンプルプログラムはこちら: http://www.piece-me.org/archive/powtest2-src.zip 現在のところこの方法で回避できていますが、原因不明なだけに不気味です。 pow()へのパラメータの取り得る範囲が予測できず、取り得るパラメータの範囲に対して 充分なテストができないケースでは、なるべくpow()は使わない方が良さそうです。 追記:もう一つの回避方法 「pow(x, y)」は「exp(log(x) * (y)」に展開できるそうです。(Webで“べき乗”について調べました) もしも、pow()関数単体に問題があるのならば、pow()関数を使わなければ問題を回避できるはずです。 そこで、#include の直後で次のように定義して、pow()を置き換えてしまいます。 #include #define pow(x,y) (exp(log(x) * (y))) /* pow()を使わないべき乗の計算 */ サンプルプログラムはこちら: http://www.piece-me.org/archive/powtest3-src.zip この方法でも問題回避できることを確認しましたが、関数呼び出しが増えるため、 前述の回避方法よりもかなり実行速度が遅くなってしまいます。 速度が要求される場面では、前述の回避方法を使った方がいいと思います。 ところで、リンカの生成するマップファイルから、pow()関数のサイズを調べたところ、かなり大きいです。 次のような、単純な実装ではないようです。 double pow(double x, double y) { return exp(log(x) * y); } たぶん高速化のため、複雑に最適化された実装になっているのだと思います。 そこに、問題発生の原因があるのではないでしょうか? * Sat Jul 6 22:38:00 JST 2002 Naoyuki Sawa - PCEWAVEINFOは開放されない 昨年12月25日の研究記録「PCEWAVEINFOはauto変数にしてはいけない」で、 pceWaveDataOut()に渡すPCEWAVEINFO構造体は、auto変数にしてはいけない理由を調べました。 pceWaveDataOut()は再生終了時に、PCEWAVEINFO構造体のstatメンバにPW_STAT_ENDを書き込むので、 pceWaveDataOut()に渡したPCEWAVEINFO構造体のメモリ領域を別の用途に再利用したりしていると、 その領域にPW_STAT_ENDが書き込まれて、データが壊れてしまう、という理由でした。 その後、BIOS 1.14から1.18へのアップグレード時に、この問題は修正されました。 BIOS 1.18の更新内容(http://www.piece-me.com/dl/update118.html)には、次のような記述があります。 ●バグフィックス ・Wave再生で解放済みのバッファに書き込むバグ修正 また、カーネルソース(c:\usr\piece\sysdev\pcekn\snd.c)にも、次のようなコメントがあります。 v1.16 2002.01.05 MIO.H 解放済みのバッファに書き込むバグ修正 そして実際に、カーネルソースも修正されているようです。(snd.cの286行目と349行目) //newp->stat = PW_STAT_START; 要らない //wp->pwiee->stat = PW_STAT_END; 要らない このように、PCEWAVEINFO構造体の開放問題が修正されたことは知っていたのですが、 ちかごろは、効果音付きのプログラムは全部シンプルライブラリで組んでいたので、 本当にPCEWAVEINFO構造体をauto変数にしてpceWaveDataOut()が使えるのかどうか、 実験していませんでした。 先日から、久しぶりにコアAPIを使ってプログラムを組んでいます。 で、PCEWAVEINFO構造体をauto変数にしてpceWaveDataOut()を使ってみました。 ・・・やっぱり落ちます・・・ なぜ?カーネルソースを見る限り、問題のか所は修正されているようですが… 原因は、statメンバへの「書込み」ではなく、pfEndProcメンバの「参照」にありました。 PCEWAVEINFOのpfEndProcに関数アドレスを指定しておくと、WAVEデータが再生終了したときに関数がコールバックされます。 pfEndProcがNULLなら、コールバックは行われません。 いずれにせよ、サウンドドライバはどこかにpfEndProcの内容を保存しておかなければいけません。 ところが現在(1.18)の実装では、再生終了時に、pceWaveDataOut()に渡されたポインタを経由してpfEndProcの内容を 参照してしまっています。 具体的には、pceWaveDataOut()に渡されたポインタが、WAVEPLAY.pwi00にそのまま保存され(snd.c:setpwi())、 再生終了時にWAVEPLAY.pwieeに移され(snd.c:make_xwave())、このpwieeを経由してpfEndProcが参照されます(snd.c:make_xwave())。 pceWaveDataOut()に渡されたポインタの指す先のPCEWAVEINFO構造体は既に開放されていることになっているので、 pfEndProcメンバのメモリ領域もアプリケーションによって別の用途に再利用され、内容は変わっているはずです。 その結果、関数アドレスじゃないデータを関数アドレスとして取得してしまい、でたらめなアドレスへの関数呼び出しを 行ってしまい、TRAPが発生するのです。 これは、auto変数の件とは別の問題です。 たとえPCEWAVEINFO構造体をグローバル変数やスタティック変数にしていても、 再生終了するまではPCEWAVEINFO構造体の内容をうかつに変更できません。 再生中のPCEWAVEINFO(本当ならばPCEWAVEINFOは即座に開放されるはずなので、“再生中のPCEWAVEINFO”という概念自体 ありえないのですが…)のpfEndProcメンバを書き換えてしまうと、pceWaveDataOut()を呼んだときのpfEndProcではなく、 書き換えた後のpfEndProcに従ってコールバックが行われてしまうのです。 実験プログラムを作成してみました。 バイナリ: http://www.piece-me.org/archive/endproc-20020706-bin.zip ソース : http://www.piece-me.org/archive/endproc-20020706-src.zip ・PCEWAVEINFO構造体はグローバル変数にしてあります。 ・Aボタンを押すと、表示メッセージを「再生中です」に設定した後、1秒程度の効果音を再生開始します。  PCEWAVEINFO.pfEndProc=NULLにしてありますので、再生終了時にはコールバックは行われず、何も起こりません。  再生終了しても、表示メッセージは「再生中です」のままです。 ・Bボタンを押すと、PCEWAVEINFO.pfEndProcに関数アドレスを設定します。  その関数では、表示メッセージを「再生終了しました」に変更します。 ・Aボタンを押して効果音が再生されているあいだに、Bボタンを押してpfEndProcを設定してみてください。  再生終了時に、表示メッセージが「再生終了しました」に変わります。  pceWaveDataOut()を呼んだ後のPCEWAVEINFO.pfEndProcへの変更が、反映されてしまうことが確認できます。 回避方法は、次のとおりです。 ・PCEWAVEINFO構造体はグローバル変数またはスタティック変数にする。  以前のauto変数の件とは別問題ですが、再生終了時までpfEndProcの内容を保持しなければいけないので、  やはりauto変数にすることはできません。 ・再生終了まで、PCEWAVEINFOの内容を書き換えてはいけない。  PCEWAVEINFO構造体がグローバル変数またはスタティック変数なので、毎回使いまわすことになりますが、  新しいWAVE情報のセットアップ前に前回の再生が完了していなければいけません。 ・再生完了待ちを行わない場合は、pceWaveAbort()で確実に再生停止してから、PCEWAVEINFOの内容を書き換えます。   PCEWAVEINFOのセットアップ→pceWaveAbort()→pceWaveDataOut() /* 悪い例 */  という順ではダメです!   pceWaveAbort()→PCEWAVEINFOのセットアップ→pceWaveDataOut() /* 良い例 */  という手順で行わなければいけません。 * Sat Jul 6 22:38:00 JST 2002 Naoyuki Sawa - 音色ベンチマーク#2 今回は、リファレンスの記述に反して「高速」音色が通常の音色よりも速い原因を調べる予定でした。 しかし、実際に音色処理を行っている c:\usr\piece\sysdev\music\musfast.s を少し見てみたところ、 「高速」音色の方が速いのは調べるまでもなく一目瞭然です。 どうやら、「高速」音色の方が遅い、というのは、リファンレンスの単純な書き間違いだったようです。 というわけで、音色ベンチマークの話題はここまでにして、今回は別の話題を取り上げてみたいと思います。 上の記事へどうぞ。 * Fri Jul 5 00:00:00 JST 2002 Naoyuki Sawa - 音色ベンチマーク#1 「P/ECE Hand Book」の『初心者のためのアプリケーション講座・P/ECEで音楽を楽しもう!』を読んで、 MMLの使い方を勉強中です。 P/ECE開発環境の音楽ライブラリマニュアルでは理解しづらかった点も、わかりやすく解説されています。 特に、リズムパートの解説が秀逸! これまで「!!」や「?」の意味がよくわからず、どこかに書いてある説明を見落としてるのかな…と思っていました。 この記事では「!!」や「?」などの謎な表記法は使わず、「F =1」も使わずに、リズムパートを記述しています。 リズム音色も他の音色と同じように扱う。気付いてみれば簡単なことだったのですが、目から鱗です。 僕は、音楽はぜんぜんダメ(MSXの音楽環境「MuSICA」をちょっとかじっただけ)なのですが、 この機会にもう一度チャレンジしてみようかな、という気になってきてます。 と言った傍から、音楽そのものを離れて細かい話に入って行くのですが(^^; 『P/ECEで音楽を楽しもう!』の記事の音色に関する説明には、次のような記述があります。 名前に「高速」が付く音色の方が処理が重いので、 処理を軽くするためには「高速」が付かない音色を使った方がいい (086ページあたりの要約) つまり、音色@0,@1,@2を使うとゲームが遅くなるので、@3,@4,@5を使え、ということですね。 P/ECE開発環境の音楽ライブラリマニュアルにも、同様の記述があります。 しかし、ちょっと待ってください。「高速」音色の方が「低速」?何となく直感に反します。 というわけで、確かめてみることにしました。 同じ曲で、音色だけを変えたデータを、6つの音色分用意します。 それらの曲をBGMに流しながら、プログラムで1000万回空ループを回して、所要時間を計ります。 空ループが回っている間はpceAppProc()を抜けませんので、音楽ライブラリの処理の重さが直接影響するはずです。 実験プログラムはこちら: バイナリ http://www.piece-me.org/archive/musbench-20020705-bin.zip ソース  http://www.piece-me.org/archive/musbench-20020705-src.zip 上下カーソルで曲を変更し、Aボタンで計測開始を開始します。 計測終了したら、所要時間が表示されます。 結果は次のようになりました。 +−−−−−−−−−−−−−+−−−−+ |     BGM     |所要時間| +−−−−−−−−−−−−−+−−−−+ |音楽なし         |10.158秒| |音色0の曲:矩形波(高速) |13.741秒| |音色1の曲:のこぎり(高速)|14.616秒| |音色2の曲:三角波(高速) |14.976秒| |音色3の曲:矩形波    |26.240秒| |音色4の曲:のこぎり   |26.241秒| |音色5の曲:三角波    |26.245秒| +−−−−−−−−−−−−−+−−−−+ 所要時間が少ないほど、処理が軽いことを示します。 1000万回の空ループの処理は変化しないので、所要時間の差は音楽ライブラリの処理時間の差。 音色以外の曲データは同じですので、すなわち、音色の処理の重さの差ということになります。 結果を見ると、名前に「高速」が付く音色の方が圧倒的に処理が軽い=「高速」です。 「高速」音色の方が「高速」という、納得のいく結果が得られました。 なぜ、音色によってこんなにも処理の重さが違ってくるのでしょうか? (続きます…) * Fri Jun 17 00:00:00 JST 2002 Naoyuki Sawa - zlibを使う#3 (…前回からの続き) 前々回の「zlibを使う#1」で、こんなことを書きました。 | pceZlibExpand()をコピー: | pceZlibExpand()のソースをアプリケションプログラムにコピーして使う、という手はどうでしょうか。 | 可能だとは思うのですが、僕は断念しました。 | pceZlibExpand()はpexファイルの展開に特化していて、ちょっと特殊なメモリ割り当て状況を前提として作られているようです。 pceZlibExpand()の使用はあきらめて、前回「zlibを使う#2」で、オリジナルのzlibライブラリを使ってみました。 しかし実は、zlibライブラリをそのまま使う方法には、ちょっと問題があったのです。 zlibの展開ルーチンを使うには、15〜35KB程度の作業領域が必要 最近のPCは数百MBものRAMを持っているので、この程度のメモリ消費は問題にならないのですが、 P/ECEは元々256KBしかRAMを持っていないので、35KBものメモリ消費はかなりきびしいです。 pceZlibExpand()はRAMが少ない環境に特化したつくりになっていて、メモリ消費はかなり小さく抑えられているみたいです。 できればpceZlibExpand()を使いたいところなのですが、どうも使い方がよくわからない… と思っていたところ、「ぱずるのP/ECE (http://www.marchen.to/~piece/)」のまかべひろしさんから、 アプリケーションプログラムにpceZlibExpand()を組み込む方法について、サンプルプログラム等の情報を頂きました。 さっそく組み込んでみたところ、確かにpceZlibExpand()を使うことができました。まかべさん、ありがとうございました。 というわけで、自作プログラムの展開ルーチンは、zlibライブラリからpceZlibExpand()へ切り替えることにしました。 pceZlibExpand()の組み込み方については、6月28日発売の「P/ECE Hand Book」をご覧下さい。(なんていいタイミング…) zlibライブラリの方も、もうちょっとだけ使い込んでみようと思います。 * Fri Jun 15 22:00:00 JST 2002 Naoyuki Sawa - zlibを使う#2 (…前回からの続き) P/ECE用zlibライブラリを作りましょう。手順は次の通り。 zlibのソース一式をダウンロードします ------------------------------------ zlibの本家サイト「zlib Home Page」から、zlibのソース一式をダウンロードします。 http://www.gzip.org/zlib/ zlibのソース一式は、いくつかのミラーサイトからダウンロードすることができます。 僕は、こちらのミラーサイトを利用させてもらいました。 http://www.libpng.org/pub/png/src/zlib-1.1.4.tar.gz ソース一式を展開します ---------------------- ダウンロードしたファイルを展開してください。 「zlib-1.1.4」という名前のフォルダができて、その中にソース一式が展開されます。 P/ECE用Makefileを用意します --------------------------- zlib-1.1.4フォルダの中には既にMakefileがありますが、P/ECE開発環境ではそのまま使うことができません。 コンパイラオプションやライブラリ構築用のコマンドなどが、P/ECE開発環境とは違っているからです。 修正するよりも、書き直した方が早そうです。 というわけで、書き直してみたのがこちら:(前回、紹介したのと同じものです) http://www.piece-me.org/archive/zlib-1.1.4-piece.mk ダウンロードしたら、ファイル名を「Makefile」に変更してください。 そして、zlib-1.1.4フォルダの中のMakefileに、上書きコピーしてください。 コンパイルします ---------------- コマンドプロンプトを開き、zlib-1.1.4フォルダの中へ移動して、「make」とタイプしてください。 しばらく待って、「Librarian Completed」というメッセージが出たら、コンパイル完了です。 必要なファイルだけ取り出します ------------------------------ zlib-1.1.4フォルダの中には、たくさんのファイルがありますが、 アプリケーションからzlibライブラリを使うために、これらが全て必要なわけではありません。 必要なファイルは、次の三つだけです: zlib.h … 最初からあります。 zutil.h … 最初からあります。 libz.lib … さっきコンパイルしてできたライブラリがこれです。 他のファイルは今のところ必要ないので、消してしまっても大丈夫です。 それでは、zlibライブラリ使って、圧縮された画像ファイルを展開し、画面に表示してみます。 サンプルプログラムはこちら: http://www.piece-me.org/archive/zapp-20020615-src.zip (続きます…) * Fri Jun 14 22:20:00 JST 2002 Naoyuki Sawa - zlibを使う#1 P/ECEの実行ファイル(以下、pexファイルと呼びます)は、フラッシュメモリの容量を節約するために、zlibで圧縮してあります。 通常のpexファイル作成手順では、コンパイル・リンクしてできたファイル(*.srf)、タイトル文字、アイコンデータ(*.pid)を、 ppack.exeを使ってまとめて、pexファイルに変換します。 このとき、srfファイルの部分が、zlibによって圧縮されます。 pexファイルが圧縮されているので、P/ECE上で実行するためには、これを展開しなければいけません。 P/ECEカーネルには、pexファイルを展開するための関数「pceZlibExpand()」が用意されています。(inflate.c) 残念ながら、アプリケーションプログラムからpceZlibExpand()を呼び出すことはできません。 もしアプリケーションからもpceZlibExpand()のような圧縮ファイル展開APIが使えたなら、役に立つ場面はかなりあると思います。 例えば、グラフィックデータや音楽データをプログラムといっしょにコンパイル・リンクせず、 独立した別ファイル(*.pgd,*.pmd)にしたり、まとめてファイルパック形式(*.fpk)にすることがあります。 プログラムとデータを分けると、場面・場面で必要なデータだけをロードできるので、たくさんのデータを使うことができます。 しかしこの方法では、データファイルにはppack.exeによる圧縮がかからないので、全てpexファイルに詰め込んだ場合に較べて、 フラッシュメモリ容量ををたくさん消費する、という欠点があります。 もし圧縮ファイル展開APIが使えたなら、データファイルも圧縮しておいて、必要な部分だけを展開しながらロードすることで、 フラッシュメモリ容量を節約できるはずです。 前述の通り、現状ではアプリケーションプログラムからpceZlibExpand()を呼ぶことができないので、自前で用意しましょう。 いくつか、方法があります。 おでマル圧縮を使う: おでマルは、非常にたくさんのグラフィックデータを使うために、データファイル(odemaru.grp)を圧縮して持っています。 圧縮してもP/ECEのフラッシュメモリ容量ぎりぎりですが… おでマルのデータファイルは、pexファイルの圧縮に使われているzlibとは違う、独自の方法で圧縮されています。 展開プログラムは、おでマルのソースファイルに含まれているので、これをコピーして使うことができそうです。 が、残念ながら、圧縮プログラムがありません。 よって、この方法は却下です。 pceZlibExpand()をコピー: pceZlibExpand()のソースをアプリケションプログラムにコピーして使う、という手はどうでしょうか。 可能だとは思うのですが、僕は断念しました。 pceZlibExpand()はpexファイルの展開に特化していて、ちょっと特殊なメモリ割り当て状況を前提として作られているようです。 zlibを使う: というわけで、今回はzlibの勉強も兼ねて、オリジナルのzlibソースからP/ECE用zlibライブラリを構築してみることにしました。 これまで、zlibを使ったアプリケーション(gzipやunzipなどの圧縮・展開ツール、png画像形式など)はいろいろ使ってきましたが、 zlibライブラリそのものをプログラムから直接利用したことはなかったので、いい機会です。 P/ECE用zlibライブラリを構築する、といっても、特に難しい点はありませんでした。 ソース一式をダウンロードして、P/ECE用のMakefileを書いただけです。 プログラム修正などの移植作業は、一切必要ありませんでした。 P/ECE用zlibライブラリ構築用のMakefileはこちら http://www.piece-me.org/archive/zlib-1.1.4-piece.mk 詳しくは次回に。 (続きます…) * Wed Jun 12 21:30:00 JST 2002 Naoyuki Sawa - S1C33208(#2) (…前回からの続き) 「S1C33208」と「S1C33209」の特徴を、データシートで比較してみました。 +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |                  |                S1C33208                 |                  S1C33209                  | +==================+=========================================+============================================+ |CMOS LSI 32ビット並列処理|S1C33000(=E0C33000)RISCコア                |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |メインクロック           |60MHz(Max.、外部クロック入力 15MHz                |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |サブクロック            |32.768kHz(Typ.) 水晶発信                     |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |命令セット             |16ビット固定長、直交性の良い105種類の命令                  |←                                           | |                  |積和演算命令(MAC命令 2サイクル実行)                    |                                            | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |内蔵RAM容量           |8,192バイト                                 |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |クロックタイマ           |1ch.                                     |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |プログラマブルタイマ        |8ビット×4ch.、16ビット×6ch.                     |8ビット×6ch.、16ビット×6ch.                        | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |PWMタイマ            |16ビットプログラマブルタイマにより実現                     |(記載無し)                                      | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |ウォッチドッグタイマ        |16ビットプログラマブルタイマにより実現                     |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |シリアルインターフェース      |2ch.                                     |4ch.                                        | |                  |クロック同期式・調歩同期式を選択可能                       |クロック同期式・調歩同期式を選択可能                          | |                  |赤外線(IrDA)インターフェイスとしても使用可能                |赤外線(IrDA)インターフェイスとしても使用可能                   | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |10ビットA/D変換器       |逐次比較方式 入力8ch.                            |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |高速DMA             |4ch.                                     |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |インテリジェントDMA       |128ch.                                   |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |汎用入出力ポート          |入力13ビット、出力29ビット                          |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |割り込みコントローラ        |外部割込み:10種類                               |←                                           | |                  |内部割込み:29種類                               |                                            | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |外部バスインターフェース      |アドレス24ビット、データ16ビット、チップイネーブル7本            |←                                           | |                  |DRAM、バーストROM直結可能                         |                                            | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |出荷形態              |QFP5−128pin/QFP15−128pin                 |←                                           | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |電源電圧              |内部動作電圧:1.8〜3.6V                          |←                                           | |                  |I/O電圧 :1.8〜5.5V                          |                                            | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |消費電流              |HALT時:27μA(3.3V、32.768kHz クロックタイマ動作 Typ.)|SLEEP時:10μA  (3.3V、32.768kHz クロックタイマ動作 Typ.)| |                  |                                         |      :2.5μA (2.0V、32.768kHz クロックタイマ動作 Typ.)| |                  |通常動作時:65mA(3.3V、50MHz Typ.)              |通常動作時 :65mA  (3.3V、50MHz Typ.)              | +−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ ご覧の通りほとんど同じですが、いくつか違っている項目もあります。 これらの項目が、S1C33208からS1C33209への変更点なのでしょうか? ・PWMタイマ  S1C33208のデータシートには記載があるのに、S1C33209のデータシートには記載がありませんでした。  しかしこれは単に、S1C33209データートの記載漏れ(または単に“特徴”から外しただけ)だと思います。  現にP/ECEは、16ビットプログラマブルタイマを使って、PWM方式のサウンド出力を行っています。  というわけで、PWMタイマに関しては、S1C33208とS1C33209は同じと考えていいと思います。 ・消費電流  通常動作時の消費電流は同じですが、省電力モードの消費電流の記載が異なっています。  S1C33208はHALT時(ちょっと省電力なモード)、S1C33209はSLEEP時(すごく省電力なモード)の値なので、単純に比較できません。  S1C33209のデータシートには様々な条件でのHALT時の消費電流も載っているので、比較は可能だと思うのですが、知識不足でよくわかりませんでした。  僕にはまだ、消費電流を気にするほどのハード知識がないので、この話題はまたいずれ興味が出たときに取り上げることにします。 ・プログラマブルタイマ  8ビットタイマが増設され、S1C33208の4チャネルから、S1C33209では6チャネルになりました。  新しく追加されたのは、Ch.4とCh.5です。  S1C33209データシートのI/Oポートアドレス表を見ると、Ch.0〜3の設定用I/Oポートは並んで集まっているのに、  Ch.4〜5設定用I/Oポートだけが離れたところにあったりするのは、後から追加されたからだったのですね。  さて、「P/ECE研究室〜S1C33分室 8ビットプログラマブルタイマ」の回: http://www.piece-me.org/piece-lab/s1c33/20020115.html  でも取り上げましたように、Ch.4〜5は、Ch.0〜3に較べて、ちょっと機能が限定されています。  端子へのクロック出力や、タイマ割り込みの発生源として利用できない(または利用しづらい)のです。  Ch.4〜5を追加した目的は何だったのでしょうか?それは… ・シリアルインターフェース  シリアルインターフェースが増設され、S1C33208の2チャネルから、S1C33209では4チャネルに倍増しました。  なるほど、計測機器をつないだりする場合、2チャネルではちょっとこころもとない。  4チャネル欲しい、という要望があったのかも知れません。  新しく追加されたのはCh.2とCh.3で、既存のCh.0〜1と同じ機能を備えています。  シリアルインターフェースを使うには、どこからかクロックを供給しなければいけません。  内部クロックを使う場合、シリアルインターフェースCh.0は8ビットタイマCh.2から。  シリアルインターフェースCh.1は8ビットタイマCh.3から、クロック供給を得ます。  8ビットタイマCh.0とCh.1は既に用途が決まっています(DRAMリフレッシュ、A/D変換開始トリガ、OSC3の発信安定待ち時間など)ので、  8ビットタイマCh.0〜1を使ってシリアルインターフェースCh.2〜3にクロックを供給することはできません。  また、16ビットタイマは汎用的なつくりなので、シリアルインターフェースへのクロック供給のような専用的な用途には不向きのようです。  そこで、シリアルインターフェースCh.2〜3へのクロック供給のために、8ビットタイマにもCh.4〜5が追加されたのだと思います。  この用途に特化しているので、他の既存の8ビットタイマCh.0〜3よりも機能が限定されているのですね。 結論。S1C33208からS1C33209への変更点: 「シリアルインターフェースが、2チャネルから4チャネルに増えました。」 (増えたチャネルのために、8ビットタイマも2チャネル追加されました。) * Tue Jun 11 21:00:00 JST 2002 Naoyuki Sawa - S1C33208(#1) P/ECEのCPU「S1C33209」の周辺回路を直接操作する実験プログラムを作成するために、 I/Oポートアドレス定義ファイル「c33208.h」を使ってきました。 この定義ファイル、名前からもわかるように、S1C33209用ではありません。 同じS1C33シリーズのCPUですが、S1C33209よりも旧いモデル「S1C33208」用の定義ファイルです。 c33208.hの内容と、S1C33209のデータシートを見較べたところ、両者の内容の大部分は合っています。 しかし一部、データシートには載っているのに、c33208.hには定義されていないI/Oポートもあります。 S1C33209がS1C33208のマイナーアップグレード版であることはたぶん間違いないのですが、 具体的にどこが変わったか?に関する資料が、これまで見つけられませんでした。 というのも、本家EPSONのデータシート置き場: トップページ http://www.epsondevice.com/domcfg.nsf -> ドキュメント http://www.epsondevice.com/www/jp/doc.nsf -> 32-bitマイクロコンピュータ (S1C33 Family) http://www.epsondevice.com/www/PDFS/epdoc.nsf/selectView?OpenForm&32mcu (2002/06/11現在) には、既にS1C33208に関する資料はなく、Webを検索してみても、ほとんど資料が見つからないのです。 c33208.hの中のコメントから推察するに、S1C33208が登場したのはたぶん1999年頃だったと思われます。 また、こちらのロードマップ: http://www.epson-esec2002.com/prdct/MCU.html (2002/06/11現在) によると、S1C33209の登場は2001年です。(P/ECEの登場と同じ年ですね) S1C33208は二年間も使われていたはずなのに、この情報の少なさはいったい… S1C33209も検索にヒットするのはほとんどP/ECEがらみなので、もともとマイナーなシリーズなのかも? 半ばあきらめていたのですが、今日、S1C33208のデータシートを見つけることができました。 これまで「S1C33208」の名前で検索していたのが、なかなか見つけられなかった原因でした。 旧名称の「E0C33208」で検索すればよかったのですね。 情報が少ないことに変わりはありません(Google検索でたった2ページ)が、先頭にPDFがヒットしました。 というわけで、S1C33208(旧名称E0C33208)のデータシートはこちら: http://www.2k1.co.uk/products/epson/downloads/ds_33208.pdf (2002/06/11現在) なぜここだけに残っているのか不思議ですが、とりあえず残っているうちに捕獲しておきましょう。 S1C33208とS1C33209のデータシートを見較べると、やはり周辺回路がいくつか強化されているようです。 前置きが長すぎたため、本題に入ったところで今日はおしまい。 (続きます…) * Sat Jun 01 20:35:00 JST 2002 Naoyuki Sawa - 一部書き換えは不可 最近になって、やっとpceFile系APIを使い始めました。 今回は、pceFileWriteSct()を使っていて気付いたことのメモです。 セクタの一部だけを、pceFileWriteSct()を使って書き換えることはできません。 例えば、次のような内容のテキストファイル「hello.txt」をP/ECEに転送しておきます。 hello, world このファイルの一部を、pceFileWriteSct()を使って書き換えようとしてみます。 pceFileOpen(&fa, "hello.txt", FOMD_WR); pceFileWriteSct(&fa, "HELL!", 0, 5); 先頭から5文字分に書き込みますので、結果は HELL!, world となりそうに見えますが、実際にはこうなります。 HELL! ・・・・ 「HELL!」よりも後ろは、ゴミになってしまいます。 なぜこうなるのでしょうか? pceFileWriteSct()の内部動作は、次のようなものです。 ・指定されたセクタを、いったんぜんぶ消去する。 ・指定された長さの分だけ、データを書き込む。 従って、指定されたセクタのうち、書き込まなかった部分の既存のデータは消去されてしまいます。 消去されないようにするには、アプリケーションプログラム側で対策しなければいけないようです。 ・変更したいデータを含むセクタ全体(4096バイト)を、RAMに読み込む。(pceFileReadSct使用) ・RAMに読み込んだデータを、必要に応じて変更する。 ・変更が終わったら、セクタ全体(4096バイト)をファイルに書き戻す。(pceFileWriteSct使用) * Fri May 31 21:00:00 JST 2002 Naoyuki Sawa - make メモ P/ECE開発環境に入ってる「make」ユーティリティは、一般的なUnix上のmakeや、VisualC++のnmake等に較べて、 一部、機能制限されたものとなっています。 機能制限されている点につきましては、同じくP/ECE開発環境に入ってる資料の 『S1C33ファミリCコンパイラパッケージマニュアル』(C:\usr\PIECE\docs\datasheet\EPSON\S5U1C33000C_J.pdf) の「17.1 make (463ページ〜)」を参照してください。 率直に言って、P/ECE開発環境のmakeは、かなり機能制限されています。 でも僕の場合、元々、GNU makeなどの一般的なmakeは、機能豊富すぎて全然使いこなせていませんでした。 何度マニュアルを読んでも、$*と$<と$@の違いが覚えられなかったり…(歳かなTT) P/ECE開発環境のmakeぐらいが、僕にはちょうどいいかも知れません。 さて、P/ECE開発環境のmakeを使っていて、ちょっと気付いたことをメモしておきます。 ------------------------------------------------------------- コメントの開始を示す「#」は、行の先頭に書かなければいけません ------------------------------------------------------------- 例えば、Unix上のmakeならば、次のようなコメント行を書くことができます。 all: # *** この行はコメント *** echo "OK!" しかしP/ECEのmakeでは、「# *** この行はコメント ***」をコマンドとして実行しようとして、エラーになります。 VisualC++のnmakeでも、P/ECEのmakeの場合と同じように、エラーになります。 では、Unix上のmakeでは、なぜエラーにならなかったのでしょうか? Unixでは、「#」この文字がメイクファイルに対するコメント開始文字であると同時に、 シェルに対するコメント開始文字でもあるからです。 Unix上のmakeも、「# *** この行はコメント ***」をコマンドとして実行しようとしますが、 コマンドの実行を仲介するシェルが、「#」この文字以降をコメントと見なして捨ててくれるようです。 つまり、Unix上のmakeでエラーならなかったのは、たまたま、と言えるかもしれません。 (「#」が行の先頭になければいけないって、P/ECEのmakeでエラーに遭遇するまで知りませんでした...(^^;) 上の例を正しく書き直すと、次のようになります。 all: # *** この行はコメント *** echo "OK!" なんとなく見づらいですが、仕方がありません。 あるいは次のような手段で、行の途中からコメントを始めることもできますが、ここまでやる必要はなさそう。 all: cmd /c rem *** この行はコメント *** echo "OK!" なお、依存関係行に関しては、行の途中から「#」でコメントを始めても大丈夫です。 次の例は、P/ECEのmake、nmake、Unix上のmakeのいずれでも、正しく動作します。 all: # *** この部分はコメント *** echo "OK!" ---------------------------------------------------------------------- 別のフォルダにあるメイクファイルを実行するには、-Cオプションを使います ---------------------------------------------------------------------- Unix上のmakeなら、次のようにするところ、 cd lib; make all P/ECEのmakeではエラーになってしまいます。 Windowsのシェルは、「;」を使ったマルチステートメントに対応していないためです。 P/ECEのmakeで、上の例と同じことを行うには、 make -Clib all と書きます。 -Cオプションを使うと、指定されたフォルダへ移動してからメイクファイルを読み込みます。 -Cオプションは、Unix上のmakeにはない、P/ECE開発環境のmakeに特有のオプションみたいです。 * Fri May 26 00:00:00 JST 2002 Naoyuki Sawa - 関数アドレスをシンボル定義して呼び出す 「関数ポインタ型にキャストしてはいけない」の回(以下、前回と略します)の続きです。 絶対アドレスcallを行うには、インラインアセンブラを使って、次のように: asm("xld.w %r1, 0x00c034aa"); asm("call %r1"); 書かなければいけない、というのが、前回の結論でした。 ちなみに0x00c034aaは、BIOS 1.18における、pceLCDTrans()のアドレスです。 関数アドレスを直接プログラム中に記述すると、プログラムが読みづらくなります。 上のコードは、次のように書ければ、読みやすくなるでしょう。 asm("xld.w %r1, pceLCDTrans"); asm("call %r1"); そこでまずは、カーネル内の関数アドレス一覧を定義した、.hファイルを用意します。 カーネル内の関数アドレスは、「c:\usr\piece\sysdev\pcekn\pcekn.sym」から得られますので、 スクリプトを使って.hファイルに変換しましょう。 pcekn.symを.hファイルに変換するスクリプトはこちら http://www.piece-me.org/archive/pcekn_sym.sed (※前回のサンプルプログラムのおまけフォルダに入ってたのと、同じものです) 使い方は、次の通りです: sed -nf pcekn_sym.sed c:\usr\piece\sysdev\pcekn\pcekn.sym > pcekn_sym.h (※P/ECE開発環境を「c:\usr\piece」以外にインストールした場合は、適宜変更) これで、カーネル内の関数アドレス一覧を定義した、pcekn_sym.hファイルが作成されます。 以下に、pcekn_sym.hの一部を示します: #define pcekn_BootEntry00 0x00c02004 #define pcekn_BootEntry 0x00c02078 #define pcekn_pceGUID 0x00c02048 #define pcekn_InitVector 0x00c02208 #define pcekn_pceVectorSetTrap 0x00c021aa #define pcekn_pceVectorSetKs 0x00c021d2 #define pcekn_UnusedTrapVector 0x00c021fa #define pcekn_UnusedKsVector 0x00c021fe シンボル名の先頭に「pcekn_」というプレフィクスを付けて定義することにしました。 このようにした理由は、シンボル名をそのまま使うと、公開されているAPIの名前と ぶつかってしまうことがあるからです。(pceVectorSetTrapやpceVectorSetKsがそうです) さて、pcekn_sym.hを使って、次のように書くことが出来るでしょうか: #include "pcekn_sym.h" (略) asm("xld.w %r1, pcekn_pceLCDTrans"); asm("call %r1"); 実は、このように書くことはできません。 「pcekn_pceLCDTrans」というシンボルは、Cプリプロセッサによって展開されるのですが、 asm("〜")文の中は単なる文字列と見なされるようで、展開の対象外だからです。 pcekn_pceLCDTransは、Cプリプロセッサの段階で展開されなければいけません。 そこで、次のような関数を用意します。 int call(int addr) { asm("call %r12"); } S1C33の仕様では、関数への引数は、%r12,%r13,%r14,%r15,およびスタックに渡すことになっています。 引数addrは既に%r12レジスタに入っているので、そのまま「call %r12」とすればいいのです。 また、S1C33の仕様では、戻り値がある場合、%r10,および%r11レジスタに返すことになっています。 「call %r12」の呼び出し先からの戻り値は、そのままcall()関数の呼び出し元へ返されるのです。 {{ちょっと寄り道、C言語の話 call()関数の戻り値は、上記のような動きによって、直接%r10レジスタに格納されるため、 intを返す関数であるにもかかわらず、return命令がありません。 C言語のプログラムとしては間違っているように見え、コンパイラが警告かエラーを出しそうなものですが、 実際には、コンパイラは警告もエラーも出しません。 この場合は、警告もエラーも出なくてうれしいのですが、なぜ出なかったのでしょうか? 「プログラミング言語C第2版」を読み直してみたところ、どうやらこれは、C言語として正しいようです。 『値を返す関数に「return 式」がなかったり、単なる「return」で抜けたりするのは、文法的には正しい』 ということがわかりました。(1.7「関数」の、31〜32ページあたりです) 例えば次のような関数は、いずれもC言語の文法としては正しいのです。 int sum(int a, int b) { } /* ゴミを返す */ int sum(int a, int b) { return; } /* ゴミを返す */ int sum(int a, int b) { return a + b; } /* 正しい結果を返す */ う〜む、これは知りませんでした。姑息なコーディングの手段として、使えそうだ…(^^; ちなみにC++言語では、文法の仕様が厳しくなっているので、エラーになります。 }}寄り道終了 call()関数を利用して、pceLCDTrans()を呼び出すには、次のようにします。 call(pcekn_pceLCDTrans); 最適化オプションや、その他のコンパイルオプションを付けたり外したりして試してみたところ、 どの場合にも、正しく動くようです。 今回のサンプルプログラムはこちら http://www.piece-me.org/archive/direct2-20020526.zip * Mon May 20 00:00:00 JST 2002 Naoyuki Sawa - ファイルから一文字づつ読み込む P/ECEには、ファイルからデータを読み込むAPI「pceFileReadSct()」があります。 このAPIの欠点として、 ファイルの4KB単位の位置からしか読み込み開始できない という点が挙げられます。例えば、 「ファイルの先頭から8191(=8KB-1)バイト目のデータを1バイトだけ使いたい」場合、 いったん、バッファに4096バイト目〜8191バイト目のデータを読み込んで、 バッファの中の4095バイト目を取り出す、なんてことをしなければいけません。 pceFileReadSct(&fa, buffer, 8191 / 4096, 4096); data = buffer[4095]; 時間的にも(1バイト使いたいだけなのに、4KBの転送を行っています)、 空間的にも(1バイト使いたいだけなのに、4KBのバッファが必要です)、非常にムダです。 そこで、pceFileReadSct()には、この欠点を帳消しにする、素晴らしい機能が備わっています。 読み込むバッファのアドレスにNULLを指定すると、フラッシュメモリ上でのセクタアドレスを 直接、得ることが出来るのです。後は、フラッシュメモリ上のデータに直接アクセスすればOK。 この機能を利用して、ファイルから一文字づつ読み込むサンプルプログラムを作ってみました。 http://www.piece-me.org/archive/read-20020519.zip フラッシュメモリ上のデータに直接アクセスできるこの機能、すごく気に入ってます。 設計者の英断に拍手! 今回のネタは、「"P/ECE" Official WebPage」の「開発者掲示板」より頂きました。 * Fri May 17 22:30:00 JST 2002 Naoyuki Sawa - 関数ポインタ型にキャストしてはいけない 前回の実験で、InitFlashAcc()を直接アドレス指定して、呼び出しました。 そのとき気付いたのですが、pcc33(P/ECE開発環境付属のCコンパイラ)で、 APIアドレスを直接指定して呼び出す場合、微妙なコーディングの違いと 最適化の有無によって、期待通りの結果にならないことがあるようです。 結論から言いますと、 「関数のアドレスを示す数値を、関数ポインタ型にキャストしてはいけない」 みたいです。 厳密なC言語の仕様としては、これは正しく動かなくて正解なのかも知れませんが、 前述の通り、微妙なコーディングの違いや最適化オプションの有無によって、 動いたり動かなかったりするので、ちょっとつまづきました。 それでは、例を挙げます。 pceLCDTrans() APIのアドレスは、BIOS 1.18では、0x00c034aaとなっています。 このアドレスを直接呼び出すCプログラムは、直感的には次のようになるでしょう。 <例1.c> ((void (*)())0x00c034aa)(); ところがこれは、最適化の有無に関わらず、正しく動作しません。 pcc33に-bオプションを指定して、Cコンパイラが生成したアセンブラソースを 見てみると、次のようになっています。 <例1.ps> xcall 0x00c034aa 一見、正しく見えますが、実は正しくないのです。 C33(P/ECEのCPU)では、直値を引数に取るcall命令は、相対アドレスcallです。 つまりこれは、「この命令の次のアドレス+0x00c034aa」へのcall命令なのです。 コンパイラかアセンブラが、0x00c034aaは絶対アドレスだと認識していれば、 相対アドレスへの変換が期待できるのですが、そうはなっていないようです。 C33で絶対アドレスcallを行うには、アドレス値をいったんレジスタに代入し、 レジスタを介してcallする必要があります。そこで、次のように書いてみます。 <例2.c> void (*fn)() = (void (*)())0x00c034aa; fn(); すると、おおよそ期待通りに、次のようなアセンブラソースが生成され、 <例2.ps> xld.w [%sp+4],%r0 xld.w %r10,0x00c034aa xld.w [%sp],%r10 xld.w %r0,[%sp] call %r0 正しく動作します。どうやら、アドレスを直接指定してAPIを呼び出すには、 関数ポインタ型の変数にアドレスを代入してから、呼び出せばいいようです。 ところが!! pcc33に最適化スイッチ-O2を付けると、これも正しく動かなくなります。 -O2を付けてコンパイルした場合のアセンブラソースは、こうなります。 <例2.ps 最適化> xcall 0x00c034aa ご覧の通り、例1と同じアセンブラソースが生成されています。 コンパイラの最適化により、変数への代入が省略されてしまったからです。 ※そもそもC言語仕様に反することをやっているので強くは言えないのですが、 ※この最適化は問題だと思います。 ※前述の通り、C33では「call 直値」と「call レジスタ」のふるまいは、 ※全く異なっています。最適化によって、 ※ 「直値 -> 変数; 変数 -> レジスタ; call レジスタ」 ※という処理を、 ※ 「call 直値」 ※に置き換えることは絶対に出来ないはずです。しかし、実験結果を見る限り、 ※pcc33(が呼び出すgcc33)は、この置き換えを行ってしまうようです。 結局、最適化の有無によらず正しく動作させるためには、インラインアセンブラで 書くしかないようです。 <例3.c> asm("xld.w %r1, 0x00c034aa"); asm("call %r1"); 生成されたアセンブラソースは、当然、次のようになります。 <例3.asm> xld.w %r1, 0x00c034aa call %r1 最適化スイッチ-O2を付けても、もちろん、結果は変わりません。 <例3.asm 最適化> xld.w %r1, 0x00c034aa call %r1 今回の実験プログラムはこちら http://www.piece-me.org/archive/direct-20020517.zip * Thu May 16 12:00:00 JST 2002 Naoyuki Sawa - フラッシュメモリAPIをムリヤリ使う P/ECEうたわれるもの計画の一環で、フラッシュメモリの直接書き換えを試しました。 なぜこんなことを試したかというと、大きな画像データをフラッシュメモリ上に 置いたまま利用するために、実行時にファイルシステムのデフラグを行うためです。 さて、フラッシュメモリを直接書き換えるためのAPIとして、 pceFlashErase()とpceFlashWrite()が用意されています。(ただし非公式です) pceFlashErase()とpceFlashWrite()は、piece.hに載っているところを見ると、 ユーザーアプリケーションから使えるように見えますが、実は使えません。 カーネルサービスベクタに登録されていないからです。 pceFlashErase()とpceFlashWrite()をカーネルサービスベクタに登録するための InitFlashAcc()は定義されている(fmacc.c)のですが、どこからも呼ばれていません。 そのため、pceFlashErase()とpceFlashWrite()に対応するベクタエントリは、 デフォルトのUnusedKsVector()を指したままになっています。 UnusedKsVector()は何もせずに-1を返す関数(pcekn.c)なので、 ユーザーアプリケーションがpceFlashErase()やpceFlashWrite()を呼び出すと、 何もせずに-1を返すだけなのです。 BootEntry()初期化関数(pcekn.c)から、各種サブシステムの初期化が呼ばれ、 カーネルサービスベクタが設定されます。 InitFlashAcc()もここから呼ばれるべきだと思うのですが、呼び忘れでしょうか。 それとも、フラッシュメモリの書き換えは危険なので、ユーザーアプリケーションから 利用できないように、わざと呼んでいないのでしょうか。 ベクタエントリは用意されているところを見ると、InitFlashAcc()の呼び忘れの 可能性が高いように思うのですが…(わざとだったらごめんなさい) pceFlashErase()とpceFlashWrite()をユーザーアプリケーションから使うには、 何とかしてInitFlashAcc()を呼び出し、カーネルサービスベクタを設定してやらなければいけません。 今回は、非常に危険な方法ですが、アドレス決め打ちで呼び出すことにしました。 BIOSのバージョンが変わると、アドレスもずれるので、多分動かなくなると思います。 実験プログラムはこちら http://www.piece-me.org/archive/flash-20020516.zip 実験用ファイルを作成し、その内容を、ファイルAPIではなくフラッシュメモリAPIを使って、 直接書き換えています。(※前述のとおり、BIOS 1.18 専用です!) * Mon Feb 4 20:16:32 JST 2002 Naoyuki Sawa - カーネルいちゃもんコーナー出張版(ライブラリ編) 久々に、シンプルライブラリでミニゲームを作ってみようと思いました。 PieceSystem Ver1.18で追加された、シンプルライブラリの新機能も使ってみたいですね。... C:\usr\PIECE\lib\simple.lib: Warning: Unresolved external symbol 'va_end'. C:\usr\PIECE\lib\simple.lib: Warning: Unresolved external symbol 'va_start'. ...それ以前の問題でした。リンクできなくなってます(;;) 再現方法は次の通りです。付属のsimpleプログラムで、prog.txtの二行目を、 printstr("SKI GAME"); から、 siprintf("SKI GAME"); に変更してmakeすると、上記のメッセージが出て、実行ファイルは生成されません。  僕たちのP/ECE開発環境では、va_startやva_endはマクロとして定義されているのですが、 シンプルライブラリの作成環境では、va_startやva_endが関数になってたのかも知れません。 PieceSystem Ver1.18がリリースされて、もう一週間以上経っていますので、 とっくに発覚していると思いますし、次回のリリースでは修正されると思いますが... シンプルライブラリはお手軽・高機能で、かなり気に入ってただけに、ちょっと痛いところです。 * Tue Jan 29 20:20:03 JST 2002 Naoyuki Sawa - おでマル高速版 昨日の研究記録でちょっと予告(?)した通り、おでマルに高速版pceLCDDrawObjectを組み込んでみました。 昨日の時点では、転送元ビットマップは2bitデータmask付き・反転なし、だけに対応していましたが、 おでマル高速版に組み込んだものは、転送元ビットマップは2bitデータmask付き、または、maskなし。 上下左右反転にも対応しました。コード展開しまくった結果、8KB近いサイズ増加になってしまいました。 オリジナルのpceLCDDrawObjectが、あまり高速化されていなかった理由が、よく理解できました。 全てのプログラムが高速描画を要求するわけでもないのに、あまり大きなルーチンにできないですよね。 さて、おでマルベンチの結果発表です。同じセーブデータを使って、サウンドONの状態でおでかけし、 おでかけ開始〜帰還までの時間を計測しました。アイテム発見回数の違いも影響しますが、ある程度の 目安にはなると思います。あ、pceAppSetProcPeriodとかの調整はしていないので、信じてください(^^; STATUS: POW=652 INT=886 DEX=682 2930J アクアプラスビルへおでかけ〜帰還まで おでマル(本物) -> 8分45秒 おでマル高速版 -> 5分50秒 数字で見るとあまり高速化されていませんが、サウンドOFFにかなり近い速度にはなったと思います。 もっとも、おでマル高速版の意義はそれ自体にあるのではなく、 「これぐらいあおっておけば、だれか(本家でも可)pceLCDDrawObjectを徹底的に高速化してくれるのでは?」 という点にあったりします。よろしくお願いします〜(^^; * Tue Jan 29 20:20:03 JST 2002 Naoyuki Sawa - カーネルいちゃもんコーナー出張版 BIOS1.18現在、pceLCDDrawObjectの2bitデータ(mask無し)描画には不具合があるようです。 再現プログラムはこちら。(または、トップページのリンクから) http://www.piece-me.org/archive/drawerr-20020129.zip 再現方法: drawerr.pexを転送し、実行してください。64x16ピクセルの2bitデータ(mask無し)を左右反転で描画する、単純なプログラムです。 パッドの上下左右でキャラクタを動かすことができます。左右に動かすと、時々キャラクタが崩れるのが確認できると思います。 プログラムを終了するには、Aボタンを押してください。 原因: pceLCDDrawObjectは、2bitデータ(mask無し)を描画するとき、特定の条件で高速ルーチンを使うように作られているようです。 高速ルーチンが使われる条件は、 ・転送先X座標が4の倍数であること。 ・転送元X座標が4の倍数であること。 ・転送幅が4の倍数であること。 これらの条件が全て満たされると、テーブルを利用して4ピクセルまとめて描画を行い、三倍程度の高速化が達成されています。 しかしながら、左右反転時には4ピクセルを左右逆転したテーブルを利用すべきなのですが、現在の実装ではそうなっていません。 左右反転しないときと同じテーブルを使ってしまっているため、4ピクセル移動するごとにキャラクタが崩れる結果となります。 回避方法: この不具合が現在まで残っているということは、あまり2bitデータ(mask無し)は使われていない、ということでしょうか? 僕もあまり使いません(^^; ・2bitデータ(mask無し)は使わない。2bitデータmask付きで代用する。ただし、少し速度低下します。 ・2bitデータ(mask無し)を描くときは、左右反転は使わない。 * Mon Jan 28 21:09:06 JST 2002 Naoyuki Sawa - 二倍速くなりました 昨日に引き続き、pceLCDDrawObjectの高速化の話題です。 昨日の研究記録では、既存の最適化済みルーチンを使わせて頂くのも手かな、と書きましたが、 まずは練習のために、自分でも実装してみることにしました。 高速版pceLCDDrawObjectを組み込んだソースはこちら http://www.piece-me.org/archive/tstspd-20020128-src.zip 高速版pceLCDDrawObjectの部分は、こんな感じです。 ----------------------------------------------------------------------------- #define KSNO_LCDDrawObject 100 /* pceLCDDrawObjectのカーネルサービス番号 */ static int (*old_LCDDrawObject)(DRAW_OBJECT); /* 本物のpceLCDDrawObjectを保存 */ static int new_LCDDrawObject(DRAW_OBJECT obj) { PIECE_VRAM dest; PIECE_BMP src; int n, x, y, step, dest_stride, src_stride; unsigned char *dest_buf, *src_mask, mask; unsigned short *src_buf, pixel; /* 単純転送でなければ、本物に任せます。 */ if(obj.param != DRW_NOMAL) return old_LCDDrawObject(obj); /* 転送元ビットマップ情報を取得。 * 2bitマスク付きビットマップでなければ、本物に任せます。 */ src = *obj.src; if(src.header.bpp != 2 || src.header.mask != 1) return old_LCDDrawObject(obj); /* 転送先VRAM情報を取得。 */ if(obj.dest != NULL) { dest = *obj.dest; } else { dest.w = DISP_X; dest.h = DISP_Y; dest.buf = pceLCDSetBuffer(INVALIDPTR); } /* 転送先クリッピング。 */ if(obj.dx < obj.clip.left) { n = obj.clip.left - obj.dx; /* 左にはみ出たピクセル数 */ obj.dx = obj.clip.left; obj.dw -= n; obj.sx += n; } if(obj.dx + obj.dw > obj.clip.right) { obj.dw = obj.clip.right - obj.dx; } if(obj.dw <= 0) return 0; /* 画面外 */ if(obj.dy < obj.clip.top) { n = obj.clip.top - obj.dy; /* 上にはみ出たピクセル数 */ obj.dy = obj.clip.top; obj.dh -= n; obj.sy += n; } if(obj.dy + obj.dh > obj.clip.bottom) { obj.dh = obj.clip.bottom - obj.dy; } if(obj.dh <= 0) return 0; /* 画面外 */ /* 転送元クリッピングは行いません。 * 元々指定された転送元矩形が転送元ビットマップ内に収まっていたならば、 * 転送先クリッピングによって転送元がはみ出すことはありえないからです。 * しかし本物は転送元クリッピングも行っているので、この点は非互換です。 */ /* 転送先ピクセルアドレスを求めます。 */ dest_buf = dest.buf + dest.w * obj.dy + obj.dx; dest_stride = dest.w /* ラインストライド */ - obj.dw; /* 一行で何回進むか */ /* 転送元ピクセルアドレスを求めます。 */ n = (src.header.w * obj.sy + obj.sx) / 8; src_buf = (unsigned short*)src.buf + n; src_mask = src.mask + n; src_stride = (src.header.w + 7) / 8 /* ラインストライド */ - ((obj.sx & 7) + obj.dw - 1) / 8; /* 一行で何回進むか */ /* ~~~ 8ステップ完了時に進むのではなく、9ステップ目の先頭で進むから */ y = obj.dh; /* 残りライン数 */ do { /* 8ピクセル単位のうち、何ピクセル目から開始するか? */ step = obj.sx & 7; /* 開始点から始まるか途中から始まるかわからないので、 * 初回はたとえstep=0でも、ここで最初のデータを取得することにします。 * ※P/ECEビットマップのピクセル並びは、MSB側が左のピクセルなんですね。 * S1C33はリトルエンディアンなんだから、LSB側を左のピクセルにした方が扱いやすいと思うのですが。 * WindowsBMPはMSB側が左のピクセルなのかな?もしそうならば、BMPの仕様に合わせたのかも知れません。 */ pixel = *src_buf; /* ピクセル並びは MSB:45670123:LSB の順です */ mask = *src_mask; /* ピクセル並びは MSB:01234567:LSB の順です */ /* 何ピクセル目から開始するかによって、適切な位置へ飛び込みます。 */ x = obj.dw; /* 残りピクセル数 */ switch(step) { LOOP: /* 初回はここは飛び越します */ /* 8ピクセル単位の開始点で、次のデータを取得します。 */ pixel = *++src_buf; mask = *++src_mask; #define PIXEL(p, m) \ /* 不透明ピクセルなら、描きます。 */ \ if(mask >> (m) & 1) *dest_buf = pixel >> (p) * 2 & 3; \ dest_buf++; \ /* 1ライン転送完了したら、抜けます。 */ \ if(--x == 0) break; case 0: PIXEL(3, 7); case 1: PIXEL(2, 6); case 2: PIXEL(1, 5); case 3: PIXEL(0, 4); case 4: PIXEL(7, 3); case 5: PIXEL(6, 2); case 6: PIXEL(5, 1); default/*7*/: PIXEL(4, 0); goto LOOP; #undef PIXEL } /* 転送先・転送元ピクセルアドレスを次のラインへ進めます。 */ dest_buf += dest_stride; src_buf += src_stride; src_mask += src_stride; } while(--y != 0); return 1; } void hook_LCDDrawObject() { /* pceLCDDrawObjectをフックします。 */ old_LCDDrawObject = pceVectorSetKs(KSNO_LCDDrawObject, new_LCDDrawObject); } void unhook_LCDDrawObject() { /* pceLCDDrawObjectをフック解除します。 */ pceVectorSetKs(KSNO_LCDDrawObject, old_LCDDrawObject); } ----------------------------------------------------------------------------- さて、結果発表です。 本物pceLCD(抜き有り) -> 21.207秒 ... 100% 高速pceLCD(抜き有り) -> 9.718秒 ... 218% ※これです! pclSprite (抜き有り) -> 6.091秒 ... 348% まあまあです。だけど、スプライトライブラリには遠く及びません。 しかも、スプライトライブラリのソースには「未高速化」とコメントされています。 スプライトライブラリは、今後まだまだ速くなるということでしょうね。手強い! 願わくば、その余力でちょっとだけpceLCDDrawObjectも高速化して欲しいところですが...(^^; 今回実装した高速版pceLCDDrawObjectは、次の条件が満たされた場合のみ高速ルーチンを利用し、 一つでも満たされなかった場合は、本物のpceLCDDrawObjectに処理を任せるようにしました。 高速ルーチンが利用される条件は、次の通りです。 ・pceLCDSetObjectに指定したparamが、DRW_NOMALであること。 ・転送元ビットマップが、「2bitデータmask付き」であること。 この条件を満たす描画要求(高速ルーチンが利用されます)の回数よりも、 条件を満たさない描画要求(本物に処理を任せます)の回数の方が圧倒的に多い場合は、 高速版pceLCDDrawObjectから本物のpceLCDDrawObjectを呼び出す処理が増える分、 元々の処理速度よりも遅くなってしまう可能性があります。 また、本物のpceLCDDrawObjectに較べて、一点、処理を省いた部分があります。 それは、転送元ビットマップに対する転送元矩形のクリッピング処理です。 転送元矩形に、転送元ビットマップをはみ出す領域を指定した場合は、正しく動作しません。 計測に使ったテストプログラムは、高速版pceLCDDrawObjectにかなり有利な条件が揃っています。 そこで、実際のアプリケーションが本当に速くなるのか確かめるために、 高速版pceLCDDrawObjectを使って、ソルダムもどきをビルドし直してみました。 試してみたところ、ソルダムが積みあがったときやゲームオーバー時のソルダムが崩れるところなど、 前バージョンでは非常に遅くなっていた部分の処理速度が、ある程度改善されていました。 おでマルかBlackWingsに組み込んで確かめた方がフェアなのでしょうけれど、それはまたいずれ、です。 今回の高速ルーチンは、アセンブラを使っておらず、極端なループ展開も行っていません。 ストレートな実装であまり工夫もない(というか、このへんが僕の限界です^^;)ので、 まだまだ高速化の余地があると思います。自作プログラムの動作速度がなぜか遅い、 と感じておられる方は、pceLCDDrawObjectの高速化に挑戦してみてはいかがでしょうか。 * Sun Jan 27 21:10:10 JST 2002 Naoyuki Sawa - pceLCDDrawObjectはスプライトライブラリの3.5倍遅い!         か? これまで、似たような処理の落ちものパズルを二本作りました(どちらも途中ですが^^;)。 描画関数として、最初の方はスプライトライブラリ、後の方は基本的なpceLCD系関数を使いました。 そのとき感じたこと: 「pceLCDDrawObjectって、スプライトライブラリに較べてすごく遅くないですか?」 というわけで今回、実際に速度比較を行ってみました。テスト条件は次の通りです。 ・背景は真っ白とします。 ・8x8ピクセルのキャラクタを、一画面当たり5000個表示します。 ・8x8ピクセルの内訳は、半分が抜きで、残り半分を四色で等分します。 ・10回画面更新して、かかった時間を計ります。 ソースはこちら http://www.piece-me.org/archive/tstspd-20020127-src.zip tstspd.cの最初のほうにある、 #define SPRITE をコメントアウトするとpceLCDのテスト、定義するとスプライトライブラリのテストになります。 それでは結果発表です。 pceLCDDrawObject -> 21.207秒 スプライトライブラリ -> 6.091秒 なんと、3.5倍違います! 参考数値として、抜き無しのデータを使った場合のpceLCDDrawObjectの時間は、 pceLCDDrawObject(抜き無し) -> 14.713秒 それでも抜き有りのスプライトライブラリの速度に2.5倍近く負けています。 なお、スプライトライブラリは抜き有り固定なので、抜き無しの時間は計れません。 ワンP/ECEくらぶさん(http://ueno.cool.ne.jp/ueno/6408/piece/index.htm)のベンチマーク結果によると、 最終的な画面転送に用いるpceLCDTransとpceLCDTransDirectの違いだけでは、ここまで差がつかないはずです。 ということは、この差は、pceLCDDrawObjectとスプライトライブラリの描画性能の違いだと考えられます。 もっとも、汎用性重視のpceLCD系関数と、制限付きで高速化を目指したスプライトライブラリを、 単純に比較するのはフェアではありません。しかし逆に言えば、pceLCDDrawObjectの基本的な機能しか 使わないのならば、専用化した自前の関数に置き換えて、かなりの高速化が図れるのではないでしょうか。 ワンP/ECEくらぶさんのベンチマーク記事には、高速な描画ルーチンも紹介されていましたので、 これを使わせて頂いてインターフェイスだけpceLCDDrawObject互換にする、というのも手かもしれません。 ともかく、pceLCDDrawObjectの速度は、何とかしたいところです。 * Fri Jan 25 18:45:18 JST 2002 Naoyuki Sawa - ぐれいと!アップデート&カーネルいちゃもんコーナー出張版(ツール編) 来ました、P/ECEアップデートです。まだざっと眺めただけですが、今回もすごいですよ! 特に目玉な更新点をいくつか挙げてみようと思ったのですが、どれも目玉で選びきれません。 仕方が無いので、最高中の最高の目玉だけを。P/ECE複数台接続対応、ブラボー! 早速、mini ISDの改良に取り掛かります。とり急ぎ、複数台接続に対応したバージョンを、 本日中にアップロードすることをお約束いたします。今回は単純な改造だけになりますが、 いずれP/ECEの数だけウインドウを開けるようにしたいです。(まだ知識不足でできません^^;) 他にも赤外線改良、マルチスレッド対応、ラインスクロール追加、etc、etc、... 開発者なら狂喜乱舞しそうな内容が目白押し。置いてかれないように、調査開始です。 [追記] pieceif.dllの問題点について P/ECEを接続しない状態で、次のプログラムを実行してみてください。 ---------------------------------------------------------------------- #define STRICT #include #include #include "c:/usr/PIECE/tools/isd/pieceif.h" #pragma comment(lib,"c:/usr/PIECE/tools/isd/pieceif.lib") int main() { fprintf(stderr, "一回目 -> %d\n", ismInitEx(0, PIECE_DEF_WAITN)); fprintf(stderr, "二回目 -> %d\n", ismInitEx(0, PIECE_DEF_WAITN)); return 0; } ---------------------------------------------------------------------- 一度目は正しく失敗するのですが、二度目は成功してしまいます!P/ECEつながってないのに。 pieceif.cの315行目で失敗を検出したときに、pu->hMutexを開放していないのが原因ではないかと思います。 とりあえず回避策として、 ・ismInitEx()を呼んだら、それが例え失敗してもismExit()を呼ぶ。 または、 ・ismInitEx()を呼ぶ直前で、必ずismExit()を呼ぶ。 僕は、後者で対応しました。 * Thu Jan 24 06:00:00 JST 2002 Naoyuki Sawa - 赤外線通信に手を出しました まずは、P/ECEの赤外線通信に取り組んでおられる、先駆者の方々のWebページを読みました。 ...どうやら、赤外線通信APIに真正面から取り組むと、相当な苦労が避けられないようです。 なぜ、P/ECEの赤外線通信は、 ・そんなに遅いのでしょうか? ・そんなに間違うのでしょうか? ・全二重通信でないのでしょうか? これらの疑問を解決するために、カーネルソースを読んでみることにしました。 ...難解です。僕には高度すぎて理解できません。 わかる部分だけから推測すると、P/ECEの赤外線通信は次のように行われていると思います。 ・送信部の発光パターンは、高速点滅が一定時間続くのと、消灯が一定時間続くのの、繰り返し。 ・点滅と消灯のワンペアを1波形として、この波形の長さで0のビットと1のビットを表す。 波長によってデータを表すので、同じサイズのデータでも、内容によって送信に必要な時間が異なります。 ★★★ 例によって、僕は赤外線通信に関して、全くの素人です。 ★★★ だから、どうしてこんな複雑なことをしているのかわかりませんでした。 ・送信するデータのビットパターンを、単純に点灯(1)・消灯(0)に対応させて、 ・非同期シリアル通信のような方法を採った方が簡単だし、速いのではないか? ・回路図を見ると送信部と受信部は独立しているから、全二重通信ができるのでは? しかし、いくつかの実験と、赤外線通信に関するWebページを読んで、それは不可能とわかりました。 まず一つ目の疑問。単純に点灯(1)・消灯(0)としてはいけないのか?そうできない理由、それは、 赤外線を出しているのがP/ECEだけではなく、自然の光や蛍光灯からも出ているからです。 単純に赤外線を受光したら1のビットとすると、明るいところではずっと1のままになるかも知れません。 だから、自然の光と間違えにくい、38KHzの点滅を利用しているのですね。なるほど、納得です。 38KHzというのはP/ECE特有の仕様ではなく、赤外線通信の一般的な決まりごとみたいです。 この速度が選ばれた理由の一つは、蛍光灯の点滅を間違えて認識してしまわない速度なのだそうです。 P/ECEの送信部は単なるLEDなので、38KHzのクロック生成には16ビットタイマを利用しています。 これに対し、P/ECEの受信部は、それ自体が38KHzの赤外線しか認識しないようになっているようです。 二つ目の疑問。非同期シリアル通信と同じ方法ではダメなのか?例えば9600bpsと仮定すると、 1のビットなら1/9600秒間38KHzの点滅を継続し、0のビットなら1/9600秒間消灯を維持する。 8ビットごとにスタートビットとストップビットを付加。受信側は9600Hzの割り込みでサンプリング。 9600Hzの割り込みはキツイように感じますが、現状の赤外線APIだって38KHzの割り込みを使っています。 以上のような方法ではダメなのか?これがダメな理由は、どうやら送信部ではなく受信部にあるようです。 受信部は、38KHzの赤外線を受信している間1を出力し、受信していない間0を出力する、のではありません。 受信なし状態から受信あり状態へ変化した瞬間に、パルスを送出するだけです。パルス幅に意味はありません。 つまり、いま38KHzの赤外線を受信しているか?という静的な情報は得られないようなのです。 確かにこの仕様でデータを送るには、波長を変えることによってパルス発生間隔を変えるしかありません。 さらにWeb上の情報を調べたところ、赤外線通信では、波長を変えるのが一般的な方法みたいです。 単にON/OFFの切り替えでは、赤外線の特性上、何か問題が起きるのかも知れません。 このあたりについては、引き続き勉強中です。 ※ON/OFFによる表現は無理でも、1/9600秒の単位時間内にパルスが来たかどうか、で代用できないかな?  失敗する可能性大ですが、ちょっと実験してみたいと思います。 ※現在のカーネルは、パルスを検出するのになぜかレベルトリガに設定し、わざわざ片方を捨てています。  エッジトリガではダメなのでしょうか?この疑問を解決するためにも、さらに実験が必要みたいです。 三つ目の疑問。送信部と受信部を同時に使って、全二重通信はできないのか。 実験してみたところ、データを送信すると、なぜか自分自身でもそのデータを受信してしまうのです。 送信部だけを指でふさいで試してみると、自分が送ったデータを受信する変な動作はなくなります。 これはどういうことでしょうか? 最初、相手のP/ECEか部屋の壁に赤外線が反射して、赤外線が戻ってきているのかと考え、 空に向かって送信してみました。...ダメです。受信してしまいます。ということは、 送信部から出た光が、直接、隣の受信部に入ってしまっているとしか考えられません。 (受信部から送信部が見えているようには思えないのですが...ケースの中では見えているのでかな?) 使い勝手を良くするために、送信部の照射角度を広くし、受信部の受光角度も広くすると、 こうなってしまうのは仕方がないのでしょうか。他の赤外線通信機器ではどうなのか、気になるところです。 いずれにせよ、全二重通信の希望は完全に潰えました。 [追記] 良く考えたら、ケースの中で見えているのなら、指で窓をふさいで受信しなくなるはずはないですね。 さらにその後の実験で、指でふさいでも受信してしまうケースが多発しました。 また、USB電源で使っていると、何もしていなくてもパルスが来てしまうようです。 どうやら、単純に送信部の光が受信部に入っている、という原因ではないみたいです。 もうすこし、調査が必要です。 今回の赤外線通信の実験は、16ビットタイマの実験も兼ねていました。 16ビットタイマの実験については、近々、S1C33分室にて報告しようと思います。 * Wed Jan 16 12:33:19 JST 2002 Naoyuki Sawa - お宝、発見! EPSONさん、ごめんなさい。 前回、S1C33のI/Oポートアドレス定義ファイルは提供されていないのではないか? と書いてしまいましたが、しっかり提供されていました。 しかも、僕が作ったような#defineによるアドレスの羅列ではなく、 構造体とビットフィールドを使って周辺回路エリアの各ビットまで定義した、 非常に高度なものでした。 この定義ファイル、P/ECEのインストールフォルダに入ってました。(はずかし〜) C:\usr\PIECE\docs\datasheet\EPSON\ccEVv4.exe を解凍したディレクトリの中の、 SAMPLE\hdr33208\c33208.h です。 ちょっと型番が違うのが気になりますが、末尾の1番くらいなら、きっとマイナーチェンジでしょう。 ざっと眺めた感じでは、I/Oポートアドレスやビット構成は、S1C33209と同じようです。 というわけで、僕のs1c33io.hは忘れてください。(^^; c33208.hを使いましょう! 僕も早速、昨日書いた8ビットタイマの実験プログラムを、c33208.hを使って書き直してみるつもりです。 ちょっと気をつけなければいけない点として、割り込み要因クリアビット(bINT_F8T_F8TU0)等に ビットフィールドでアクセスすると、実際にはリード/モデファイ/ライト動作になってしまうので、 まだ処理していない(発生した瞬間の)割り込み要因までクリアしてしまう恐れがあります。 ヤバそうなところはビットフィールドの一つ手前、バイト単位のpINT_F8Tを使って、 ビット操作は自分で明示的に書いた方が安全かも知れません。 ccEVv4.exeには、他にも、周辺回路のサンプルプログラム等がどっさり入ってます。 これらのサンプルプログラムを自分で理解するのが、今後のS1C33分室の方針となりそうです。 楽しくなってきました! 追記: SAMPLE\drv33208\includeフォルダの中には、ビットフィールドを使わない、 単純なI/Oポートアドレスとビットマスクの定義も、入っています。 場合によっては、こちらを使った方がわかり易いかも知れません。 * Thu Jan 13 12:25:01 JST 2002 Naoyuki Sawa - I/Oポートアドレス mini ISDが一段落したので、Windows側のプログラムはひとまず終了です。 また、P/ECE本体の勉強に戻ることにしました。 さて、あちこちのI/Oポートを操作するプログラムの実験をする時に、 I/Oポートアドレスの値を直接プログラム中に書くのは、間違いの元です。 簡単のために、I/Oポートアドレスを定義したヘッダファイルが、 メーカー(EPSONですね)から提供されているのではないかと思うのですが、 カーネルソースも全て直接アドレス値で書かれているところを見ると、 もしかしたら、提供されていないのかも知れません。 そこで、I/Oポートアドレスを定義したヘッダファイルを作ってみました。 http://www.piece-me.org/archive/s1c33io.h 付属のs1c33209_221_222j.pdfから抜き出して整形しただけなのですが、 実はこのPDFファイル、テキストの抜き出しが禁止されていたのを 無理やり抜き出してしまったので、ちょっと問題ありかも知れません。 * Thu Jan 3 12:25:01 JST 2002 Naoyuki Sawa - やっぱり2000の方が安定してました P/ECEの開発環境を、Windows Meから2000に移行しました。 まだ半日ほどしか使っていませんが、感想: 「USBがすごく安定したような気がする...」 Meでは、P/ECEを付け外しした拍子に、なぜかUSB接続ができなくなり、 そのままWindowsまで固まってしまう、という現象が日に数回ありました。 対策として、P/ECEをスリープした状態ではPCに取り付けない、 きちんとメニューに戻ってから取り外す、などに気をつけていたのですが、 それでもやはり固まるときは固まっていたのです。 2000にしてからは、かなり乱暴に付け外ししても、ぜんぜん問題なし。 これまで、NT系Windowsにはあまりいい印象がなかったのですが (だってゲーム動かないもん)、こんなに快適になるとは思いませんでした。 もしかして、XPってもっと快適なんでしょうか?