+===========+ |P/ECE研究室| +===========+ P/ECE研究記録 ============= * Thu Sep 09 00:00:00 JST 2021 Naoyuki Sawa - GNU Awk 3.1.7の不具合を回避する 僕はAwkが好きです。 P/ECEのアプリ開発でデータを作る時も、仕事で簡単なテキスト処理をする時も、よくAwkを使っています。 もっとモダンなスクリプト言語も勉強したのですけれど、しばらく使わないとすぐに忘れてしまって、ついAwkとSedに戻ってしまいます…(^^; 僕が使っているAwkは、「GNU Awk 3.1.7(windows special Nov 24 2009)」(gawk-mbcs-win32-20091124.zip)です。 古いバージョンなのですけれど、もっぱらシフトJISのテキストだけを扱う上では使い勝手が良いので、重宝しています。 しかし残念な事に、ダウンロードページが公開停止中です。 (参照:「corbieのブログ」(http://blog.livedoor.jp/corbie/)さんの「Windows版gawk選び」(http://blog.livedoor.jp/corbie/archives/3924154.html)の記事) もうダウンロードできなさそうなので、大事に使って行こうと思います。 長らく使ってきて大きな問題は起きていなかったのですけれど、最近、ローカル配列を使うと不具合が発生する事に気付きました。 Awkでローカル配列を使うには、以下のようにします。 ■テスト� \犠� │BEGIN { │ a[1] = 100 │ print "呼び出し前: " a[1] │ test() │ print "呼び出し後: " a[1] │} │function test(a) { │ a[1] = 200 │ print "呼び出し中: " a[1] │} □結果 │呼び出し前: 100 │呼び出し中: 200 │呼び出し後: 100 また、配列の末尾に値を追加して行くには、以下のようにする事が多いです。 ■テスト�◆\犠� │BEGIN { │ delete a #//確実に空の配列にする。 │ a[length(a)+1] = 200 │ a[length(a)+1] = 300 │ a[length(a)+1] = 400 │ for(i = 1; i <= length(a); i++) { │ print i ": " a[i] │ } │} □結果 │1: 200 │2: 300 │3: 400 �,皚△眄気靴�動作するのですが、�,鉢△鯀箸濆腓錣擦董▲蹇璽�ル配列の末尾に値を追加して行こうとすると、不具合が発生しました。 ■テスト�� 異常 │BEGIN { │ a[1] = 100 │ print "呼び出し前: " a[1] │ test() │ print "呼び出し後: " a[1] │} │function test(a,i) { │ delete a #//確実に空の配列にする。 │ a[length(a)+1] = 200 │ a[length(a)+1] = 300 │ a[length(a)+1] = 400 │ for(i = 1; i <= length(a); i++) { │ print i ": " a[i] │ } │} □結果 │呼び出し前: 100 │1: │2: 300 │3: 400 │4: │5: │6: │7: │8: │9: │10: │…終わらない… 調査した所、どうやら、ローカル配列に値を格納する前にdeleteすると、配列の内部構造が壊れて、length()が不定な値を返すようです。 ■テスト�ぁ^枉� │BEGIN { │ a[1] = 100 │ print "呼び出し前: " a[1] │ test() │ print "呼び出し後: " a[1] │} │function test(a) { │ delete a │ print "呼び出し中: " length(a) │} □結果 │呼び出し前: 100 │呼び出し中: 1804120 │呼び出し後: 100 ちなみに、グローバル配列に値を格納する前にdeleteしても大丈夫なので、上記の不具合はローカル配列の場合だけに発生するようです。 ■テスト�ァ\犠� │BEGIN { │ a[1] = 100 │ print "呼び出し前: " a[1] │ test() │ print "呼び出し後: " a[1] │} │function test() { │ delete a │ print "呼び出し中: " length(a) │} □結果 │呼び出し前: 100 │呼び出し中: 0 │呼び出し後: 僕はこれまで、ローカル配列をあまり使わずに、グローバル配列ばかり使っていたので、たまたま不具合に遭遇していなかったようです。 ためしに、MinGW版のGNU Awk 3.1.7(gawk-3.1.7-1-msys-1.0.11)でもテストしてみた所、同じ不具合が発生しました。 どうやら、「GNU Awk 3.1.7(windows special Nov 24 2009)」に特有の不具合ではなく、元のGNU Awk 3.1.7に起因する不具合のようです。 ちなみに、「GNU Awk 4.1.4, API: 1.1」では不具合は発生しませんでした。 GNU Awk 3.1.8以降のどれかのバージョンで修正されたのだと思います。 (GNU Awk 3.1.8のChangeLogを読んでみたのですけれど、わかりませんでした…) 不具合を回避するためには、本来は、普段使いのAwkをもっと新しいバージョンに変えるべきなのです。 でも、「GNU Awk 3.1.7(windows special Nov 24 2009)」に慣れ過ぎていて、微妙な挙動が変わるのが怖くて、変えたくありません… そこで、「GNU Awk 3.1.7(windows special Nov 24 2009)」のままで、不具合を回避する方法を考えました。 ローカル配列に値を格納する前にdeleteすると不具合が発生するのですから、deleteする前にローカル配列に値を格納すれば良いのです。 ■テスト�Α\犠� │BEGIN { │ a[1] = 100 │ print "呼び出し前: " a[1] │ test() │ print "呼び出し後: " a[1] │} │function test(a) { │ a[1] = 1 #//ダミー │ delete a │ print "呼び出し中: " length(a) │} □結果 │呼び出し前: 100 │呼び出し中: 0 │呼び出し後: 100 ■テスト�А\犠� │BEGIN { │ a[1] = 100 │ print "呼び出し前: " a[1] │ test() │ print "呼び出し後: " a[1] │} │function test(a,i) { │ a[1] = 1 #//ダミー │ delete a #//確実に空の配列にする。 │ a[length(a)+1] = 200 │ a[length(a)+1] = 300 │ a[length(a)+1] = 400 │ for(i = 1; i <= length(a); i++) { │ print i ": " a[i] │ } │} □結果 │呼び出し前: 100 │1: 200 │2: 300 │3: 400 │呼び出し後: 100 正しい結果になりました。 忘れないようにするためには、常に関数の先頭でローカル変数を初期化するのを、定型とすれば良いと思います。 ■定型 │BEGIN { │ test(1,2,3) │} │function test(x,y,z,t1,t2,t3,a1,a2,a3) { #//x,y,zは引数。t1,t2,t3はローカル変数。a1,a2,a3はローカル配列。 │ a1[1] = 1 #//ダミー │ delete a1 #//確実に空の配列にする。 │ a2[1] = 1 #//ダミー │ delete a2 #//確実に空の配列にする。 │ a3[1] = 1 #//ダミー │ delete a3 #//確実に空の配列にする。 │ … │} 以下、補足です。 ローカル配列を使用する前のdelete自体が不要では?と思われるかも知れませんが、必要です。 ローカル配列を使用する前にdeleteしておかないと、以下のようなコードがエラーになります。 ■テスト�─.┘蕁� │BEGIN { │ a[1] = 100 │ print "呼び出し前: " a[1] │ test() │ print "呼び出し後: " a[1] │} │function test(a,n,i) { │ n = length(a) │ a[n+1] = 200 │ n = length(a) │ a[n+1] = 300 │ n = length(a) │ a[n+1] = 400 │ for(i = 1; i <= length(a); i++) { │ print i ": " a[i] │ } │} □結果 │呼び出し前: 100 │gawk: test8.awk:9: 致命的: スカラーパラメータ `a' を配列として使用しています。 エラになる理由は、未使用の変数に対してlength()を使った時点で、その変数はスカラー変数になってしまうからです。 先にdeleteしておけば、その変数は配列変数になるので、上記のエラーは発生しません。 ■テスト�� 正常 │BEGIN { │ a[1] = 100 │ print "呼び出し前: " a[1] │ test() │ print "呼び出し後: " a[1] │} │function test(a,n,i) { │ a[1] = 1 #//ダミー │ delete a #//確実に空の配列にする。 │ n = length(a) │ a[n+1] = 200 │ n = length(a) │ a[n+1] = 300 │ n = length(a) │ a[n+1] = 400 │ for(i = 1; i <= length(a); i++) { │ print i ": " a[i] │ } │} □結果 │呼び出し前: 100 │1: 200 │2: 300 │3: 400 │呼び出し後: 100 * Sun Jun 02 23:59:59 JST 2019 Naoyuki Sawa - 構造体引数の呼び出し規約がマニュアルの説明と違う □マニュアルに説明されている、構造体引数の呼び出し規約 P/ECE開発環境のCコンパイラは、構造体引数をスタックに積んで渡します。 「S5U1C33000C Manual (S1C33 Family Cコンパイラパッケージ) (Ver.4)」(C:\usr\PIECE\docs\datasheet\EPSON\S5U1C33000C_J.pdf) p.85『6.5.4 関数呼び出し』●構造体引数の扱い │引数が構造体データの場合、構造体のメンバーの値はスタックを介して渡されます。 │例: struct _foo { │ int a; │ short b; │ char c; │ }; │ callee(struct _foo foo, int d); │上記例ではdのみが引数渡し用レジスタ(R12)に格納され、fooはすべてスタックに格納されます。 □マニュアルに説明されていない、構造体引数の呼び出し規約 ほとんどの場合はマニュアルの説明どおりのコードが生成されるのですが、特定のケースで説明と違うコードが生成される事に気付きました。 特定のケースとは、『構造体のメンバが32ビット以内で,且つ,一要素だけである場合』です。 │例: struct _foo { │ int a; //shortでもcharでも同じ。 │ }; │ callee(struct _foo foo, int d); │上記例では、fooもdも引数渡し用レジスタ(R12,R13)に格納されます。 │例: struct _foo { │ int a[1]; //shortでもcharでも同じ。 │ }; │ callee(struct _foo foo, int d); │上記例でも、fooもdも引数渡し用レジスタ(R12,R13)に格納されます。 │例: struct _foo { │ int a[2]; //shortでもcharでも同じ。 │ }; │ callee(struct _foo foo, int d); │上記例ではdのみが引数渡し用レジスタ(R12)に格納され、fooはすべてスタックに格納されます。 │例: struct _foo { │ long long a; //a[1]でもa[2]でも同じ。 │ }; │ callee(struct _foo foo, int d); │上記例ではdのみが引数渡し用レジスタ(R12)に格納され、fooはすべてスタックに格納されます。 □検証 いろいろなパターンで検証して、上記のとおりの結果になる事を確認しました。 右端に「※」を付けたパターンが前述の『特定のケース』に相当し、「S5U1C33000C Manual」では説明されていない呼び出し規約となります。 ┏━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━┓ ┃構造体 │関数 │引数レジスタ │戻り値レジスタ ┃ ┠─────┬───┬───────────────┼──────┬───┬─────────────────────────┼────────────────┬───────────────┬───────────────┬──┬───────┼────────────────┬──┨ ┃要素の型 │要素数│定義例 │引数 │戻り値│定義例 │%r12 │%r13 │%r14 │%r15│[%sp+4〜] │%r10 │%r11┃ ┣━━━━━┿━━━┿━━━━━━━━━━━━━━━┿━━━━━━┿━━━┿━━━━━━━━━━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┿━━━━━━━━━━━━━━━┿━━┿━━━━━━━┿━━━━━━━━━━━━━━━━┿━━┫ ┃char │1 │struct _8_1 { int8_t a[1]; } │構造体 │無し │void callee_8_1_V_(struct _8_1 s) │最上位8ビットに構造体引数の値 │ │ │ │ │ │ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │void callee_8_1_VA(struct _8_1 s, int t) │最上位8ビットに構造体引数の値 │整数引数の値 │ │ │ │ │ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │void callee_8_1_VB(int t, struct _8_1 s) │整数引数の値 │最上位8ビットに構造体引数の値 │ │ │ │ │ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体 │構造体│struct _8_1 callee_8_1_S_(struct _8_1 s) │戻り値を格納する構造体のポインタ│最上位8ビットに構造体引数の値 │ │ │ │戻り値を格納した構造体のポインタ│ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │struct _8_1 callee_8_1_SA(struct _8_1 s, int t) │戻り値を格納する構造体のポインタ│最上位8ビットに構造体引数の値 │整数引数の値 │ │ │戻り値を格納した構造体のポインタ│ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │struct _8_1 callee_8_1_SB(int t, struct _8_1 s) │戻り値を格納する構造体のポインタ│整数引数の値 │最上位8ビットに構造体引数の値 │ │ │戻り値を格納した構造体のポインタ│ ┃※ ┃ ├───┼───────────────┼──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │2 │struct _8_2 { int8_t a[2]; } │構造体 │無し │void callee_8_2_V_(struct _8_2 s) │ │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │void callee_8_2_VA(struct _8_2 s, int t) │整数引数の値 │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │void callee_8_2_VB(int t, struct _8_2 s) │整数引数の値 │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体 │構造体│struct _8_2 callee_8_2_S_(struct _8_2 s) │戻り値を格納する構造体のポインタ│ │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │struct _8_2 callee_8_2_SA(struct _8_2 s, int t) │戻り値を格納する構造体のポインタ│整数引数の値 │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │struct _8_2 callee_8_2_SB(int t, struct _8_2 s) │戻り値を格納する構造体のポインタ│整数引数の値 │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┠─────┼───┼───────────────┼──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃short │1 │struct _16_1 { int16_t a[1]; }│構造体 │無し │void callee_16_1_V_(struct _16_1 s) │上位16ビットに構造体引数の値 │ │ │ │ │ │ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │void callee_16_1_VA(struct _16_1 s, int t) │上位16ビットに構造体引数の値 │整数引数の値 │ │ │ │ │ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │void callee_16_1_VB(int t, struct _16_1 s) │整数引数の値 │上位16ビットに構造体引数の値 │ │ │ │ │ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体 │構造体│struct _16_1 callee_16_1_S_(struct _16_1 s) │戻り値を格納する構造体のポインタ│上位16ビットに構造体引数の値 │ │ │ │戻り値を格納した構造体のポインタ│ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │struct _16_1 callee_16_1_SA(struct _16_1 s, int t)│戻り値を格納する構造体のポインタ│上位16ビットに構造体引数の値 │整数引数の値 │ │ │戻り値を格納した構造体のポインタ│ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │struct _16_1 callee_16_1_SB(int t, struct _16_1 s)│戻り値を格納する構造体のポインタ│整数引数の値 │上位16ビットに構造体引数の値 │ │ │戻り値を格納した構造体のポインタ│ ┃※ ┃ ├───┼───────────────┼──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │2 │struct _16_2 { int16_t a[2]; }│構造体 │無し │void callee_16_2_V_(struct _16_2 s) │ │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │void callee_16_2_VA(struct _16_2 s, int t) │整数引数の値 │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │void callee_16_2_VB(int t, struct _16_2 s) │整数引数の値 │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体 │構造体│struct _16_2 callee_16_2_S_(struct _16_2 s) │戻り値を格納する構造体のポインタ│ │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │struct _16_2 callee_16_2_SA(struct _16_2 s, int t)│戻り値を格納する構造体のポインタ│整数引数の値 │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │struct _16_2 callee_16_2_SB(int t, struct _16_2 s)│戻り値を格納する構造体のポインタ│整数引数の値 │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┠─────┼───┼───────────────┼──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃int │1 │struct _32_1 { int32_t a[1]; }│構造体 │無し │void callee_32_1_V_(struct _32_1 s) │構造体引数の値 │ │ │ │ │ │ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │void callee_32_1_VA(struct _32_1 s, int t) │構造体引数の値 │整数引数の値 │ │ │ │ │ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │void callee_32_1_VB(int t, struct _32_1 s) │整数引数の値 │構造体引数の値 │ │ │ │ │ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体 │構造体│struct _32_1 callee_32_1_S_(struct _32_1 s) │戻り値を格納する構造体のポインタ│構造体引数の値 │ │ │ │戻り値を格納した構造体のポインタ│ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │struct _32_1 callee_32_1_SA(struct _32_1 s, int t)│戻り値を格納する構造体のポインタ│構造体引数の値 │整数引数の値 │ │ │戻り値を格納した構造体のポインタ│ ┃※ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │struct _32_1 callee_32_1_SB(int t, struct _32_1 s)│戻り値を格納する構造体のポインタ│整数引数の値 │構造体引数の値 │ │ │戻り値を格納した構造体のポインタ│ ┃※ ┃ ├───┼───────────────┼──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │2 │struct _32_2 { int32_t a[2]; }│構造体 │無し │void callee_32_2_V_(struct _32_2 s) │ │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │void callee_32_2_VA(struct _32_2 s, int t) │整数引数の値 │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │void callee_32_2_VB(int t, struct _32_2 s) │整数引数の値 │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体 │構造体│struct _32_2 callee_32_2_S_(struct _32_2 s) │戻り値を格納する構造体のポインタ│ │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │struct _32_2 callee_32_2_SA(struct _32_2 s, int t)│戻り値を格納する構造体のポインタ│整数引数の値 │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │struct _32_2 callee_32_2_SB(int t, struct _32_2 s)│戻り値を格納する構造体のポインタ│整数引数の値 │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┠─────┼───┼───────────────┼──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃long long │1 │struct _64_1 { int64_t a[1]; }│構造体 │無し │void callee_64_1_V_(struct _64_1 s) │ │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │void callee_64_1_VA(struct _64_1 s, int t) │整数引数の値 │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │void callee_64_1_VB(int t, struct _64_1 s) │整数引数の値 │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体 │構造体│struct _64_1 callee_64_1_S_(struct _64_1 s) │戻り値を格納する構造体のポインタ│ │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │struct _64_1 callee_64_1_SA(struct _64_1 s, int t)│戻り値を格納する構造体のポインタ│整数引数の値 │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │struct _64_1 callee_64_1_SB(int t, struct _64_1 s)│戻り値を格納する構造体のポインタ│整数引数の値 │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ ├───┼───────────────┼──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │2 │struct _64_2 { int64_t a[2]; }│構造体 │無し │void callee_64_2_V_(struct _64_2 s) │ │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │void callee_64_2_VA(struct _64_2 s, int t) │整数引数の値 │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │void callee_64_2_VB(int t, struct _64_2 s) │整数引数の値 │ │ │ │構造体引数の値│ │ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体 │構造体│struct _64_2 callee_64_2_S_(struct _64_2 s) │戻り値を格納する構造体のポインタ│ │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │構造体,整数 │ │struct _64_2 callee_64_2_SA(struct _64_2 s, int t)│戻り値を格納する構造体のポインタ│整数引数の値 │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┃ │ │ ├──────┼───┼─────────────────────────┼────────────────┼───────────────┼───────────────┼──┼───────┼────────────────┼──┨ ┃ │ │ │整数,構造体 │ │struct _64_2 callee_64_2_SB(int t, struct _64_2 s)│戻り値を格納する構造体のポインタ│整数引数の値 │ │ │構造体引数の値│戻り値を格納した構造体のポインタ│ ┃ ┗━━━━━┷━━━┷━━━━━━━━━━━━━━━┷━━━━━━┷━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━┷━━┷━━━━━━━┷━━━━━━━━━━━━━━━━┷━━┛ 上の表のエクセル版: http://www.piece-me.org/piece-lab/struct/struct-20190602.xlsx 検証プログラム一式: http://www.piece-me.org/piece-lab/struct/struct-20190602.zip □構造体引数がレジスタに格納される方式 ちなみに、『特定のケース』において構造体引数がレジスタに格納される方式も、少々変則的です。 8ビットや16ビットの構造体の場合、何故か、上位ビット側に詰めて格納されるのです。 P/ECEのCPU S1C33はリトルエンディアンで、こんな格納の仕方をしても利点は無いように思います。 何故こんなコードが生成されているのか謎なのですが、もしかすると、元になったCコンパイラ(MIPSのビッグエンディアンモード?)の仕様がそのまま残ってしまっているのかも知れません。 実際の動作としては、ビットシフト命令が増えて少し性能低下しますが、結果には影響ありません。 □構造体の戻り値の呼び出し規約は、マニュアルどおり 構造体引数の呼び出し規約が、『特定のケース』において、「S5U1C33000C Manual」の説明とは異なるコードが生成される事を説明しました。 一方、構造体の戻り値の呼び出し規約については、例外は無く、「S5U1C33000C Manual」の説明どおりです。 たとえ、『構造体のメンバが32ビット以内で,且つ,一要素だけである場合』であっても、常に、説明どおりのコードが生成されます。 構造体の戻り値の呼び出し規約について、「S5U1C33000C Manual」の該当箇所を引用しておきます。 「S5U1C33000C Manual (S1C33 Family Cコンパイラパッケージ) (Ver.4)」(C:\usr\PIECE\docs\datasheet\EPSON\S5U1C33000C_J.pdf) p.85『6.5.4 関数呼び出し』●構造体を返す関数への引数渡し │構造体データを返す関数を呼び出す場合、結果を格納する構造体のアドレスが第1引数としてR12レジスタに格納され、関数に渡されます。 │したがって、ソース上で記述されている引数は1つずつ繰り下げられます。 │〜略〜 │呼び出された関数は第1引数で渡されたポインタを戻り値として返します。 □まとめ 呼び出し側関数も,呼び出される側の関数も、全部C言語で書く場合は、今回説明した点について、特に意識する必要はありません。 呼び出し側関数と,呼び出される側の関数の呼び出し規約が一致してさえいれば、問題は無いからです。 しかし、片方をC言語,片方をアセンブラで書く場合は、『特定のケース』に注意して、アセンブラ版の関数を書く必要があります。 * Wed Aug 30 02:13:02 JST 2017 Naoyuki Sawa - gcc33.exeがswitch〜caseを無駄に大きなジャンプテーブルに展開する □問題点 P/ECE開発環境のgcc33.exeは、switch〜caseを、無駄に大きなジャンプテーブルに展開する場合が有ります。 caseの値がある程度狭い範囲に収まっていて,尚且つ,飛び飛びである場合に、そうなる事が多いです。 たとえば、以下の例のように、ASCII文字の一部の文字に対して、switch〜caseで処理する場合に、そうなる事が多いです。 □C言語ソース │ int test(char c) { │ int n; │ switch(c) { │ default: n = 0; break; │ case '0': n = 1; break; │ case 'A': n = 2; break; │ case 'B': n = 2; break; │ case 'a': n = 3; break; │ case 'b': n = 3; break; │ } │ return n; │ } □gcc33.exeが生成するアセンブラコード │ .code │ .align 1 │ .global test │ test: │ xsub %r12,%r12,48 │ ld.b %r10,%r12 │ xcmp %r10,50 │ xjrugt __L72 │ xsll %r10,2 │ xld.w %r10,[%r10+__L78] │ jp %r10 │ .code │ .align 2 │ __L78: │ .word __L73 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L74 │ .word __L75 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L76 │ .word __L77 │ .code │ __L72: │ ld.w %r10,0 │ xjp __L71 │ __L73: │ xld.w %r10,1 │ xjp __L71 │ __L74: │ __L75: │ xld.w %r10,2 │ xjp __L71 │ __L76: │ __L77: │ xld.w %r10,3 │ __L71: │ ret 実行速度の最適化のためとは言え、上記のアセンブラコードは、いくらなんでも無駄が多過ぎます。 一見、そんなに大きくないように見えるかも知れませんが、機械語命令の行は1行2バイトであるのに対して、ジャンプテーブルの行は1行4バイトなので、見た目以上に大きくて無駄が多いです。 実際の所、特に高速化が必要な処理を除いて、ほとんどのswitch〜caseの処理は、if〜else ifで逐次比較しても充分な実行速度が得られる事が多いです。 だから、速度よりもサイズ節約を優先してアセンブラコードを生成して欲しいのですが、そうはなっていないようです。 ちなみに、上記のアセンブラコードは、「-O2」オプション(速度最適化)を指定してコンパイルしたものですが、試しに「-O1」オプション(サイズ最適化)を指定してみても同じコードが生成されました。 どうやら、P/ECE開発環境のgcc33.exeは、常に、サイズ節約よりも、速度を優先して最適化を行っているようです。 ×対策案1(失敗) 無駄に大きなジャンプテーブルが生成されるのを防ぐ方法を、考えてみました。 最初に考えた方法は、gccに、ジャンプテーブルを生成させないオプションを指定する方法です。 □「3 GCC Command Options - 3.18 Options for Code Generation Conventions」(https://gcc.gnu.org/onlinedocs/gcc-5.3.0/gcc/Code-Gen-Options.html)より引用: │-fno-jump-tables │ Do not use jump tables for switch statements even where it would be more efficient than other code generation strategies. │ This option is of use in conjunction with -fpic or -fPIC for building code that forms part of a dynamic linker and cannot reference the address of a jump table. │ On some targets, jump tables do not require a GOT and this option is not needed. まさに今回の目的にぴったりなオプションなのですが・・・残念ながら、使えませんでした。 どうやら、P/ECE開発環境のgcc33.exeは、バージョンが古いためか、「-fno-jump-tables」オプションに対応していないみたいです。 △対策案2(いまいち) 次に考えた方法は、switch〜caseを、if〜else ifに書き換える方法です。 □C言語ソース │ int test(char c) { │ int n; │ if(c == '0') { n = 1; } │ else if(c == 'A') { n = 2; } │ else if(c == 'B') { n = 2; } │ else if(c == 'a') { n = 3; } │ else if(c == 'b') { n = 3; } │ else { n = 0; } │ return n; │ } □gcc33.exeが生成するアセンブラコード │ .code │ .align 1 │ .global test │ test: │ ld.b %r12,%r12 │ xcmp %r12,48 │ xjrne __L2 │ xld.w %r10,1 │ xjp __L3 │ __L2: │ xcmp %r12,65 │ xjreq __L12 │ xcmp %r12,66 │ xjrne __L6 │ __L12: │ xld.w %r10,2 │ xjp __L3 │ __L6: │ xcmp %r12,97 │ xjreq __L13 │ ld.w %r10,0 │ xcmp %r12,98 │ xjrne __L3 │ __L13: │ xld.w %r10,3 │ __L3: │ ret 生成されるアセンブラコードはこれで良いのですが・・・ 既存のソースの中の、switch〜caseで書かれている部分を、手作業でif〜else ifに書き換える手間の問題が有ります。 だいたい、C言語ソースの中でswitch〜caseを使っている理由は、速度の目的よりも、書き易さ,見易さのためにそうしている事が多いので、if〜else ifに書き換えると見辛くなったりエンパグの危険が有ります。 だから、この方法も望ましくありません。 ○対策案3(成功) なるべくswitch〜caseを書き換えずに、最小限の手間で対策出来る方法を見付けました。 □C言語ソース │ int test(char c) { │ int n; │ switch(c) { │ case CHAR_MIN: /* FALLTHRU */ //おまじない ←←←←←←←←←←←←←←←←この行を追加しただけ。 │ default: n = 0; break; │ case '0': n = 1; break; │ case 'A': n = 2; break; │ case 'B': n = 2; break; │ case 'a': n = 3; break; │ case 'b': n = 3; break; │ } │ return n; │ } □gcc33.exeが生成するアセンブラコード │ .code │ .align 1 │ .global test │ test: │ ld.b %r12,%r12 │ xld.w %r10,65 │ cmp %r12,%r10 │ xjreq __L75 │ xjrgt __L80 │ xcmp %r12,-128 │ xjreq __L73 │ xcmp %r12,48 │ xjreq __L74 │ xjp __L73 │ __L80: │ xld.w %r10,97 │ cmp %r12,%r10 │ xjreq __L77 │ xjrgt __L81 │ xcmp %r12,66 │ xjreq __L76 │ xjp __L73 │ __L81: │ xcmp %r12,98 │ xjreq __L78 │ __L73: │ ld.w %r10,0 │ xjp __L71 │ __L74: │ xld.w %r10,1 │ xjp __L71 │ __L75: │ __L76: │ xld.w %r10,2 │ xjp __L71 │ __L77: │ __L78: │ xld.w %r10,3 │ __L71: │ ret 「対策案2」のアセンブラコードに比べると、多少、無駄なコードが生成されていますが、元の無駄に大きなジャンプテーブルよりはだいぶんましなので、対策としてはこれで充分だと思います。 この方法で変更した点は、「default:」の上にダミーの「case CHAR_MIN:」を追加しただけです。 処理上は「default:」にまとめられるはずなので、元と同じコードが生成されても不思議ではないのですが、何故か上手く行って、ジャンプテーブルには展開されなくなるようです。 どうやら、gcc33.exeは、処理がまとめられるかどうかに関係無く、caseの値の中にある程度狭い範囲から外れる値(この場合はCHAR_MIN)が有ると、ジャンプテーブルに展開しなくなるようです。 最適化が弱いとも言えるのですが、今回はその特性を利用して、上手く、無駄に大きなジャンプテーブルに展開されるのを防ぐ事が出来ました。 ちなみに、今回は「case CHAR_MIN:」としましたが、この値には特に意味はありません。 既存のcaseの値として使用されておらず、他のcaseの値からある程度離れている値ならば、何でも構いません。 ただし、今回のケースで「case 1000:」や「case -1000:」等としてしまうと、「warning: case value out of range」という警告が出ます。 今回のケースでは、「switch(c)」の変数cがchar型なので、1000や-1000には成り得ないからです。 今回は、変数cの範囲内で、なるべく他のcaseの値から離れている値として、CHAR_MINにしました。 実際には、対策を行うソース毎に、適切な値を検討して、試してみて、生成されたアセンブラコードを見て、無駄に大きなジャンプテーブルが生成されていないかを確認して下さい。 * Mon Aug 14 08:16:30 JST 2017 Naoyuki Sawa - ドラム音色のノイズ ■問題点 P/ECE開発環境に付属している、音楽ライブラリのドラム音色は、一部の音色でノイズが鳴ります。 ノイズが鳴る音色は、「crush cymbal」と「synth tom (hi)」と「synth tom (mid)」です。 【ノイズが鳴る事を確認するMML】 │ ;ドラムパート用マクロ設定(C:\usr\PIECE\docs\TUTORIAL\AYAKA\sample.mmlからコピー) │ !!bg @6 V125 ?o4c ; bass drum │ !!sg @8 V120 ?o4c ; snare drum 1 │ !!Sg @7 V115 ?o4c ; snare drum 2 │ !!og @9 V90 ?o4c ; open H.H. │ !!hg @10 V90 ?o4c ; closeH.H. │ !!cg @11 V110 ?o4c ; crush cymbal │ !!tg @13 V110 ?o4c ; synth tom (hi) │ !!ug @14 V110 ?o4c ; synth tom (mid) │ !!vg @15 V110 ?o4c ; synth tom (low) │ !!Cg @16 V120 ?o4c ; clap │ │ F =1 T150 l1 L !bg!sg!Sg!og!hg!cg!tg!ug!vg!Cg 上記のMMLを再生すると、6音目の「crush cymbal」と7音目の「synth tom (hi)」と8音目の「synth tom (mid)」の、鳴り終わる辺りでブチッという音が鳴る事が確認出来ます。 これまで気付きませんでした・・・ 或いは、気付いていたけれど、実用上あまり問題にならないので、見過ごしていたかも知れません。 たいていドラムパートは1トラックだけで複数のドラム音を連続で鳴らすので、上記の音色のノイズが鳴る前に次のドラム音を鳴らしていれば、実質ノイズは鳴らないからです。 ■原因 でもやっぱり気になるので、原因を調べて修正する事にしました。 ドラム音色の配列データを見てみると、最後の方に、不自然な値が入っているようです。 http://www.piece-me.org/piece-lab/wavetbl_bug/1_before_fix_h/cymbd_h.png http://www.piece-me.org/piece-lab/wavetbl_bug/1_before_fix_h/tomh1_h.png http://www.piece-me.org/piece-lab/wavetbl_bug/1_before_fix_h/tomm1_h.png これらの不自然な値を削除すれば、きっと、ノイズは消えるはずです。 しかし、なぜ不自然な値が入っていたのかがわからないので、単純にq削除して良いものかどうか、少し迷います。 そこで、これらの不自然な値が何なのか、調べてみる事にしました。 まず、ドラム音色の配列データをバイナリデータに出力して、バイナリエディタで見てみました。 http://www.piece-me.org/piece-lab/wavetbl_bug/2_before_fix_bin/cymbd_bin.png http://www.piece-me.org/piece-lab/wavetbl_bug/2_before_fix_bin/tomh1_bin.png http://www.piece-me.org/piece-lab/wavetbl_bug/2_before_fix_bin/tomm1_bin.png う〜む、わかりません。 次に、(思い付きで)、ドラム音色の配列データの値を「+128」して、バイナリデータに出力して、バイナリエディタで見てみました。 http://www.piece-me.org/piece-lab/wavetbl_bug/3_before_fix_bin_bias/cymbd_bin_bias.png http://www.piece-me.org/piece-lab/wavetbl_bug/3_before_fix_bin_bias/tomh1_bin_bias.png http://www.piece-me.org/piece-lab/wavetbl_bug/3_before_fix_bin_bias/tomm1_bin_bias.png 文字列が見えました。 どうやら、WAVファイルのツール情報のチャンクのようです。 だいたい、原因が推測出来ました。 きっと、音楽ライブラリのドラム音色を作る時に、WAVファイルを手作業で切り出すか、簡易的なスクリプト等を使って、変換したのでは無いかと思います。 その時に、WAVファイルに含まれている、音データ以外の、ツール情報のチャンクを、(うっかり?)削除し忘れてしまったのではないかと思います。 ちなみに、さきほどなぜ、ドラム音色の配列データの値を「+128」するとツール情報のチャンクが見えたのかと言うと、 WAVファイルの8ビット形式の音データは符号なし(0〜255)の値で、P/ECE音楽ライブラリの音色データは8ビット符号付き(-128〜127)の値だからです。 音楽ライブラリのドラム音色を作る時に、切り出したデータ全体を128バイアスしたために、ツール情報のチャンクも128バイアスされて紛れてしまったのだと思います。 ■対策 ノイズ部分のデータが、不要なデータだという事がわかったので、心配なく削除できます。 削除する方法は、以下の通りです。 自作プログラムに音楽ライブラリの音色データを含める時は、P/ECE開発環境のフォルダから、音色データのファイル一式をコピーして来ていると思います。 例えば、「/usr/PIECE/docs/TUTORIAL/AYAKA/wavetbl/」フォルダ等からです。(左記以外にも何箇所かのフォルダに置かれていますが、どれでも同じです。) コピーして来たファイルのうち、「i_CYMBD.h」と「i_TOMH1.h」と「i_TOMM1.h」を、下図のように書き換えればokです。 http://www.piece-me.org/piece-lab/wavetbl_bug/4_after_fix_h/cymbd_h_fix.png http://www.piece-me.org/piece-lab/wavetbl_bug/4_after_fix_h/tomh1_h_fix.png http://www.piece-me.org/piece-lab/wavetbl_bug/4_after_fix_h/tomm1_h_fix.png 上記の対策を行った後、先程と同じMMLを再生して、ノイズが鳴らなくなった事を確認出来ました。 ■ダウンロード 今回使用した検証プログラム一式の、ダウンロードはこちらです: ダウンロードはこちら: http://www.piece-me.org/piece-lab/wavetbl_bug/wavetbl_bug-20170814-src.zip * Tue Oct 25 21:18:48 JST 2016 Naoyuki Sawa - sin()のバグ ■問題点 P/ECE開発環境に付属している、EPSON製Cライブラリの、「sin()関数」には、バグがあります。 『オーバーフローする演算が直前に在ると、sin()関数の結果が不正になる。』というバグです。 【バグを再現するプログラム】 │ #include │ #include │ #include │ #define NOPCESPRINTF │ #include │ unsigned char vbuff[128*88]; │ int errno, write; /* dummy */ │ void test(); │ void pceAppInit() { │ pceAppSetProcPeriod(255); │ pceLCDSetBuffer(vbuff); │ pceLCDDispStart(); │ pceFontSetPos(0, 0); │ test(); /* テストルーチンを呼び出す */ │ pceLCDTrans(); │ } │ void pceAppProc(int cnt) { /** no job **/ } │ void pceAppExit() { /** no job **/ } │ //↑↑↑↑↑ここまでは本題と関係の無い定型処理です。↑↑↑↑↑ │ //↓↓↓↓↓↓ここからが本題のテストルーチンです。↓↓↓↓↓↓ │ volatile int x = (1<<30); │ void test() { │ char buf[100]; │ double s; │ x += x; /* オーバーフローする演算が直前に在ると… */ │ s = sin(1.57); /* sin()関数の結果が不正↓になる場合が有る。 */ │ sprintf(buf, "%f", s); /* sin(π/2)=1のはずなのに「-1」と表示される。 */ │ pceFontPutStr(buf); │ } 上記のプログラムを実行すると、sin(π/2)=1のはずなのに、「-1」と表示されます。 ■原因 原因は、EPSON製Cライブラリの「sin()関数」の、バグでした。 バグの箇所は、以下の通りです。右側のコメントを参照して下さい。 【EPSONライブラリの「sin.s」の先頭部分を抜粋】 │ ;============================================================================== │ ; Function: double sin(double x) │ ; │ ; Input: %r12: Argument x in radian double (low). │ ; %r13: Argument x in radian double (high). │ ; │ ; Output: %r10: Return value, sine of x in double (low). │ ; %r11: Return value, sine of x in double (high). │ ; │ .code │ .align 1 │ .global sin │ │ sin: ┬ここから開始して… │ pushn %r3 ; Save registers %r3...%r0. │ │ xsub %sp,%sp,20 ; Presearve working area in five words. │ │ │ │ ;[%sp+16] = Taylor series's sign parameter (high). │ │ ;[%sp+12] = Taylor series's sign parameter (low). │ │ ;[%sp+08] = Quadrant of trigonometric function (word). │ │ ;[%sp+07] = 2nd argument of modf, pointer to its fraction part (high). │ │ ;[%sp+00] = 2nd argument of modf, pointer to its fraction part (low). │ │ │ │ ld.w %r2,%r12 ; Low word of argument x. │ │ ld.w %r3,%r13 ; High word of argument x. │ │ │ │ ;##### REMOVED 12/08/1999 ############# │ │ ;# ; │ │ ;# ; Clear the sign bit of argument x in %r3:%r2 by shifting │ │ ;# ; to make it a plus value. │ │ ;# ; │ │ ;# sll %r3,1 ; Shift the argument x to the left one bit. │ │ ;# srl %r3,1 ; Shift the argument x to the right one bit. │ │ ;##### REMOVE END ##################### │ │ │ │ xld.w %r4,0x0 ; Initialize the quadrant to 0. │ │ xld.w [%sp+8],%r4 ; Initialize the quadrant to 0. │…ここまでの間に、Vフラグを変化させる命令が存在しません。 │ ↓従ってこの時点で、Vフラグは関数が呼び出された時のままで、要するに「不定」です。 │ sra %r13,1 ; Check if x is minus? ←sra命令は、ZとNフラグを変化させますが、Vフラグは変化させません。従ってこの時点でも、Vフラグは「不定」のままです。 │ xjrge _ArgIsNotMinus ; Branch if plus or zero. ←xjrge命令の分岐条件は「!(N^V)」です。『不定なVフラグを参照して条件分岐を行っている』というバグです。 │ ; │ ; --- if the argument x is minus --- │ ; Clear the sign bit of argument x in %r3:%r2 by shifting │ ; to make it a plus value. │ ; │ sll %r3,1 ; Shift the argument x to the left one bit. │ srl %r3,1 ; Shift the argument x to the right one bit. │ │ xld.w %r4,0x2 ; Set the quadrant 2. │ xld.w [%sp+8],%r4 ; Set the quadrant 2. │ ; │ ; Mutiply the argument x by the constant 2*Pai │ ; │ _ArgIsNotMinus: │ ld.w %r12,%r2 ; Argument x (low). │ ld.w %r13,%r3 ; Argument x (high). │ xld.w %r14,TWO_PAI_L ; Multiplier, constant 2*Pai (low). │ xld.w %r15,TWO_PAI_H ; Multiplier, constant 2*Pai (high). │ ; │ xcall __muldf3 ; Calculate multiplication; argument x = x * (2 * Pai). │ ; The result product is %r11:%r10. │ ld.w %r2,%r10 ; Put result x in %r2 (low). │ ld.w %r3,%r11 ; Put result x in %r3 (high). │ │ (以下略) %r13レジスタの正負を判定するのに、 ×誤 │ sra %r13,1 ; %r13 >>= 1, %psr(Z) = !%r13, %psr(V) = %r13[31] │ xjrge _ArgIsNotMinus ; if(!(%psr(N)^%psr(V))) { goto _ArgIsNotMinus } としているのが間違いです。正しくは、 〇正 │ cmp %r13,0 ; %psr(C) = 0, %psr(V) = 0, %psr(Z) = !%r13, %psr(N) = %r13[31] │ xjrge _ArgIsNotMinus ; if(!(%psr(N)^%psr(V))) { goto _ArgIsNotMinus } とするか、又は、 〇正 │ add %r13,%r13 ; %psr(C) = %r13[31], %psr(V) = ?, %psr(Z) = ?, %psr(N) = ?, %r13 <<= 1 │ xjruge _ArgIsNotMinus ; if(!%psr(C)) { goto _ArgIsNotMinus } とするか、又は、 〇正 │ scan1 %r13,%r13 ; %psr(C) = ?, %psr(V) = 0, %psr(Z) = %r13[31], %psr(N) = 0 │ xjrne _ArgIsNotMinus ; if(!%psr(Z)) { goto _ArgIsNotMinus } とすべきです。 どれを使っても効率は同なので、素直に「cmp %r13,0」を使うのが自然だと思います。 元ソースが、何故、「sra」を使って符号比較をしようとしていたのかが、謎です。(シフトした値をその後で使ってもいませんし…) ■対策 二種類の対策方法があります。 □対策� ,箸蠅△┐魂麋鬚垢訛从�方法 「sin()関数」をあまり頻繁に使わない場合は、その都度対策するのが手っ取り早くて良いと思います。 「sin(x) == cos(x-π/2)」ですから、アプリケーションプログラムのソースの中で、「sin()関数」を以下のように定義して下さい。 アプリケーションプログラムのソースの中で「sin()関数」を定義しておけば、EPSONライブラリの「sin()関数」はリンクされません。 【アプリケーションプログラムの中で定義する】 │ double sin(double x) { return cos(x - 1.5707963267948966192313216916398); } □対策�◆〆�本的に修正する対策方法 「sin()関数」を頻繁に使う場合は、その都度上記の対策を行うのは面倒だし、見落とすおそれがあるので、 EPSON製Cライブラリの「sin()関数」を修正して、置き換える方が安全でしょう。 修正した「sin.s」を、アプリケーションと一緒にビルドすれば、 「\usr\PIECE\lib\math.lib」の中にある元の「sin()関数」よりも優先されて、修正した「sin()関数」がリンクされます。 修正した「sin.s」はこちら: http://www.piece-me.org/piece-lab/sinbug/sin.s ■ダウンロード バグ再現プログラムと、対策プログラムのサンプルは、こちらです。 ダウンロードはこちら: http://www.piece-me.org/piece-lab/sinbug/sinbug-20161025-src.zip * Sat Aug 06 00:37:42 JST 2016 Naoyuki Sawa - intをlong longに符号拡張する、色々な方法 アセンブラプログラムで、int(32ビット整数)とlong long(64ビット整数)を混在して計算する場合、intをlong longに符号拡張する必要が有ります。 PCのx86 CPUだと、intをlong longに符号拡張する専用のCDQ命令が有って、あまり工夫の余地は有りません。 P/ECEのS1C33 CPUには、intをlong longに符号拡張する専用の命令は無いので、色々な方法が考えられて面白いです。 今回は、S1C33で、intをlong longに符号拡張する、色々な方法を紹介します。 例として、%r12レジスタのint値の最上位ビットを、%r13レジスタの全ビットに符号拡張して、%r13:%r12レジスタのlong long値とするケースを考えます。 �〜把召癖�法ver.1 │ ld.w %r13, %r12 │ xsra %r13, 31 一見、良さそうなのですが、実は、コードサイズが大きくて遅い、という問題が有ります。 S1C33は一度に8ビットしかシフト出来ないため、上記のアセンブラプログラムは、実際には、以下の命令に展開されるからです。 │ ld.w %r13, %r12 ;//2バイト、1サイクル │ sra %r13, 8 ;//2バイト、1サイクル │ sra %r13, 8 ;//2バイト、1サイクル │ sra %r13, 8 ;//2バイト、1サイクル │ sra %r13, 7 ;//2バイト、1サイクル コードサイズは10バイトで、実行時間は5サイクルです。 まあ、これでも構わないのですが、もっと小さく、もっと早い方法が有るので、上記の方法は却下です。 �∩把召癖�法ver.2 │ cmp %r12, 0 ;//2バイト、1サイクル │ jrge.d 3 ;//2バイト、1サイクル │ ld.w %r13, 0 ;//2バイト、1サイクル │ ld.w %r13, -1 ;//2バイト、1サイクル 分岐した場合は実行しない コードサイズは8バイトで、実行時間は「%r12≧0」の場合3サイクル,「%r12<0」の場合4サイクルです。 S1C33は条件分岐が早いので、これはかなり良い方法です。 「x = (条件) ? (値A) : (値B)」の処理をアセンブラで書く場合、遅延分岐の直後に値Aのロードを置いて、さらにその直後に値Bのロードを置く、というのが最適化の定番です。 ��キャリーフラグを使った方法 │ ld.w %r13, %r12 ;//2バイト、1サイクル │ add %r13, %r13 ;//2バイト、1サイクル │ ld.w %r13, 0 ;//2バイト、1サイクル │ sbc %r13, %r13 ;//2バイト、1サイクル コードサイズは8バイトで、実行時間は4サイクルです。 �△諒�法よりも、平均0.5サイクル遅いので、敢えてこの方法を使う必要は有りません。(利点が無いのに紹介した理由は、この後�Г如ΑΑ�) 「ld.w %r13, %r12」「add %r13, %r13」で、元の値の最上位ビットを%psr(C)に送り、「ld.w %r13, 0」「sbc %r13, %r13」で、「%r13 = 0 - 0 - %psr(C)」に相当する処理を行っています。 元の値の最上位ビットが0だったら「%r13 = 0 - 0 - 0 = 0」、元の値の最上位ビットが1だったら「%r13 = 0 - 0 - 1 = -1」となります。 �ど箙羈板イ鯣爾μ仁瓩鰺�用する方法ver.1 │ ld.w %r13, 1 ;//2バイト、1サイクル │ mlt.w %r12, %r13 ;//2バイト、5サイクル │ ld.w %r13, %ahr ;//2バイト、1サイクル コードサイズは6バイトで、実行時間は7サイクルです。 コードサイズはここまでの中で最短ですが、実行時間は遅いので、いまいちな方法です。 mlt.w命令は、int値とint値を掛けてlong long値にするので、「%r12×1」を行えば、%r12の最上位ビットが、乗算結果の%ahr:%alrの上位ワード(%ahr)に拡張される仕組みです。 �ド箙羈板イ鯣爾μ仁瓩鰺�用する方法ver.2 │ ld.w %alr, %r12 ;//2バイト、1サイクル │ div0s %r12 ;//2バイト、1サイクル │ ld.w %r13, %ahr ;//2バイト、1サイクル コードサイズは6バイトで、実行時間は3サイクルです。 div0s命令は本来は除算の前準備のための命令なのですが、その時に%alrの最上位ビットが%ahrに符号拡張される事を利用した方法です。 最初は、これが最良の方法かと思っていたのですが、致命的な問題が有る事に気付きました。 %r12が0の場合に、「div0s %r12」がゼロ除算例外を投入してしまう問題です。 この用途では、div0s命令のオペランドは関係無いので、%r12以外を指定しても構わないのですが、確実に0以外であるレジスタは有りません。 〜「ld.w %r13, 1」「div0s %r13」〜なんてしてしまうと、コードサイズと実行時間が増えて本末転倒です。 という訳で、この方法は使えません。 �α把召癖�法ver.1(改善版) │ swap %r13, %r12 ;//2バイト、1サイクル │ ld.b %r13, %r13 ;//2バイト、1サイクル │ sra %r13, 7 ;//2バイト、1サイクル コードサイズは6バイトで、実行時間は3サイクルです。 これが、最良の方法の一つです。 �,能劼戮芯未�P/ECEはシフト命令が弱いのが欠点なのですが、その欠点を回避するために、swap命令とld.b命令を組み合わせてロードと24ビットシフトを一気に行ったのが、この方法です。 swap命令の他にも、mirror命令やnot命令はロードとビット処理を同時に実行出来るので、色々工夫が効いて便利な命令です。 �Д�ャリーフラグを使った方法(改善版) │ ld.w %r13, %r12 ;//2バイト、1サイクル │ add %r13, %r13 ;//2バイト、1サイクル │ sbc %r13, %r13 ;//2バイト、1サイクル コードサイズは6バイトで、実行時間は3サイクルです。 これも、最良の方法の一つです。 ��のプログラムをよく見ると、「ld.w %r13, 0」は不要である事に気付きました。 「%r13 = 0 - 0 - %psr(C)」も、「%r13 = 不定値 - 不定値 - %psr(C)」も、結果は同じだからです。('不定値'は、両方とも同じ値とする) というわけで、��のプログラムから一命令削除したのが、この方法です。 ■まとめ P/ECEのS1C33 CPUで、intをlong longに符号拡張する方法は、上記の�λ瑤廊Г諒�法が最良だと思います。 * Fri May 27 23:10:38 JST 2016 Naoyuki Sawa - 命令エクスタンダ「ext33.exe」のバグ P/ECE開発環境の、命令エクスタンダ「ext33.exe」には、バグが有ります。 文字列の末尾に有る、エスケープされたバックスラッシュを正しく認識出来ない、というバグです。 ■バグを再現する方法 まず、正常なケースを確認します。 下記のプログラムを、"sample.c"という名前で保存して下さい。 │const char str[]="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm表現"; 下記のバッチファイルを、"compile.bat"という名前で保存して下さい。 │@REM compile.bat │pcc33.exe -b -c sample.c │@IF ERRORLEVEL 1 (ECHO エラーが発生しました。) ELSE (ECHO 正常終了しました。) コマンドラインで、compile.batを実行して下さい。 正常にコンパイルできて、以下のメッセージが表示されます。 │正常終了しました。 次に、エラーが発生するケースを確認します。 さきほど保存したsample.cを開いて、以下のように変更して、保存して下さい。 変更点は、"表"の前に"n"を一文字追加しただけです。 │const char str[]="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn表現"; コマンドラインで、compile.batを実行して下さい。 エラーが発生して、以下のメッセージが表示されます。 │ ←空行です。 │ ←空行です。 │ ←空行です。 │エラーが発生しました。 ■原因調査 文字列に一文字追加しただけで、C言語のプログラムとしては全く正しいのに、なぜ後者ではエラーが発生するのでしょうか。 調査したところ、P/ECE開発環境のext33.exeのバグが原因である事が判りました。 以下、詳細です。 pcc33.exeはコンパイラドライバであり、以下のように複数のツールを呼び出してコンパイルを行っています。 �� gcc33.exe C言語ソースをコンパイルして、アセンブラソース(拡張命令有り)に変換する。(sample.c ⇒ sample.ps) �� ext33.exe アセンブラソース(拡張命令有り)の中の、拡張命令を展開して、アセンブラソース(拡張命令無し)に変換する。(sample.ps ⇒ sample.ms) �� as33.exe アセンブラソース(拡張命令無し)をコンパイルして、オブジェクトファイルに変換する。(sample.ms ⇒ sample.o) �� lk33.exe オブジェクトファイルをリンクする。(sample.o ⇒ sample.srf) ※今回の件には関係無いので、pcc33.exeに「-c」フラグを指定して、この処理は実行しません。 �△瞭�力ファイルであるsample.psを、エラーが発生しないケースと、エラーが発生したケースとで、比較してみました。 □エラーが発生しないケース │str: │ .ascii "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklm\225\\\214" │ .ascii "\273\000" □エラーが発生したケース │str: │ .ascii "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn\225\\" │ .ascii "\214\273\000" 本来はどちらも、アセンブラソースとして正しい内容です。 後者は「表」の2バイト目の'\'が、文字列の一番最後になっています。きちんと'\\'でエスケープされているので、本来は正しいのですが、 どうやらext33.exeは、文字列を閉じる'"'を判定する時に、一文字前が'\'かどうかだけを見てエスケープされた'"'かどうかを判定しているようです。 ←←←【要点】これがバグです。 そして、文字列が閉じられていないと判定して、エラー終了してしまうようです。 試しに、上記の「□エラーが発生したケース」のsample.psを少し手で書き換えると、エラーが出なくなりました。 □エラーが発生したケース(変更後) │str: │ .ascii "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn\225" │ .ascii "\\\214\273\000" ←元々上の行の最後に有った'\\'を、次の行の先頭に移した。データの内容は全く同じで、ソース上の違いだけです。 そして、ext33.exeを直接実行してみると… │ext33.exe sample.ps │@IF ERRORLEVEL 1 (ECHO エラーが発生しました。) ELSE (ECHO 正常終了しました。) 結果は「正常終了しました。」になりました。 やはり、文字列の末尾に有る'\\'が原因です。 ちなみに、文字列が""で囲まれているかどうかを判定するのは、ext33.exeの機能の一つなのですが、その機能が正しく動いていないようですね。 □参照資料 │「S5U1C33000C Manual (S1C33 Family Cコンパイラパッケージ) (Ver.4) (P/ECE開発環境の「C:\usr\PIECE\docs\datasheet\EPSON」に入っています) │p.147「10. 命令エクステンダ - 10.8.3 文法チェック」 │>ext33は拡張命令と以下のアセンブラ疑似命令の文法チェックのみを行います。 │>〜 │>.ascii疑似命令 │> 文字列が" "で囲まれているかチェックします。 誤って、正しい文字列をエラーと見なしてしまうだけでなく、エラーメッセージを表示せずに意味不明な空行が三行出力されたり、 エラーコードを返しているにもかかわらず画面上には「Extend Completed」というメッセージを表示してしまうというバグも有ったりするようです。 ■バグの影響 一見、かなり致命的なバグのように見えますが、実際には、このバグが顕在化する事はかなり稀です。 なぜなら、C言語文字列の場合、最後にヌル文字が付くので、アセンブラソースになった時に文字列の最後に'\'が来る事は稀だからです。 │const char str1[]="明示的な\\"; │const char str2[]="暗黙の…表"; //「表」の2バイト目は'\'です。 上記のC言語文字列はどちらも'\'で終わっていますが、アセンブラソース上ではヌル文字が付くので… │str1: │ .ascii "\226\276\216\246\223I\202\310\\\000" │str2: │ .ascii "\210\303\226\331\202\314\201c\225\\\000" こんなふうに、最後が'\'にはなりません。 では、どんな時に問題が顕在化するかというと、折り返した位置が偶然'\'の直後で折り返された場合でです。 gcc33.exeは、長い文字列から.ascii疑似命令を生成する時に、ある程度の長さで折り返して複数行に分割するようです。 その時たまたま、折り返した位置が'\'の直後だと、.ascii疑似命令の文字列の最後が'\'になって、前述のext33.exeのバグに引っかかってしまうのです。 │const char str[]="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn表現"; は、まさにその一例でした。 ちなみに、もっと単純にバグを再現しようと思えば、たとえばこんなC言語文字列でも再現します。 │const char str[]="\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\"; コンパイルすると… │str: │ .ascii "\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\" ←ext33.exeが誤ってこの行をエラーと見なしてしまいます。 │ .ascii "\\\\\\\\\\\\\\\000" ■バグを回避する方法 簡単にバグを回避する方法は、思い付きませんでした。 ext33.exeの前段に、.psファイルをチェックして.ascii疑似命令の文字列の末尾に'\'が無いか調べるようなフィルタを作ろうかな、と思っています。 でも実際には、そこまでしなくても良いと思います。 これまで10年以上P/ECEで遊んでいて、今にして思えばこれが原因だったのかなあと思うコンパイルエラーに遭遇した事が、せいぜい1〜2回だけだからです。 実際にこのext33.exeバグが問題になる事は、ごく稀だと思います。 とりあえずこういうバグが有る事を覚えておいて、P/ECEのプログラムをビルドした時に謎の空行が表示されてコンパイルエラーになった場合は、この点を疑ってみるのが良さそうです。 そして、文字列を変更する事が可能ならば、文字列を少し変更して、.ascii疑似命令の文字列の末尾に'\'が来ないようにして回避すれば良いと思います。 ちなみに、文字列を変更できない場合は、文字列を配列で定義して回避するという手も有ります。 C言語で配列として定義すれば、アセンブラソースとしては.asciiではなく.byteで出力されるようだからです。 │const char str[]={'\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\','\\'}; │str: │ .byte 92 │ .byte 92 │ 〜 │ .byte 92 │ .byte 92 しかしまあ、実際にこの対策が必要になる事は無いかな、と思っています。 * Sun May 15 21:25:46 JST 2016 Naoyuki Sawa - ispunct()のバグ P/ECE開発環境のispunct()にはバグが有ります。 ispunct(' ')は正しくはfalseですが、P/ECEではtrueになります。 EPSONライブラリのバグみたいです。 ■再現プログラム #include #include #include #include #include unsigned char vbuff[DISP_X * DISP_Y]; void pceAppInit() { pceLCDSetBuffer(vbuff); pceLCDDispStart(); } void pceAppProc(int cnt) { memset(vbuff, 0, sizeof vbuff); pceFontSetPos(0, 0); pceFontPrintf("ispunct(' ') = %s", ispunct(' ') ? "true" : "false"); pceLCDTrans(); } void pceAppExit() { } ■結果 ┌──────────┐ │ispunct(' ') = true │ └──────────┘ http://www.piece-me.org/piece-lab/ispunct/ispunct1.gif 修正するには、アプリケーションプログラム内で正しいispunct()を定義して、EPSONライブラリの関数を置き換えればokです。 ■修正プログラム #include #include #include #include #include unsigned char vbuff[DISP_X * DISP_Y]; void pceAppInit() { pceLCDSetBuffer(vbuff); pceLCDDispStart(); } void pceAppProc(int cnt) { memset(vbuff, 0, sizeof vbuff); pceFontSetPos(0, 0); pceFontPrintf("ispunct(' ') = %s", ispunct(' ') ? "true" : "false"); pceLCDTrans(); } void pceAppExit() { } //--- 正しいispunct()でEPSONライブラリの関数を置き換える --- int ispunct(int c) { return ((c >= 0x21) && (c <= 0x2F)) || //EPSONライブラリは多分ここが((c >= 0x20) && (c <= 0x2F))になっている。 ((c >= 0x3A) && (c <= 0x40)) || ((c >= 0x5B) && (c <= 0x60)) || ((c >= 0x7B) && (c <= 0x7E)); } ■結果 ┌──────────┐ │ispunct(' ') = false│ └──────────┘ http://www.piece-me.org/piece-lab/ispunct/ispunct2.gif 今回使用した検証プログラム一式の、ダウンロードはこちらです: http://www.piece-me.org/piece-lab/ispunct/ispunct-src.zip * Thu Nov 26 21:59:10 JST 2015 Naoyuki Sawa - 整数型を論理型に変換する処理が、予想外に大きなコードになってしまう P/ECE開発環境のCコンパイラでは、整数型を論理型に変換する処理が、予想外に大きなコードになってしまう事があります。 ■C言語で書く場合1 例として、引数xが0ならば0(FALSE)を返し、引数xが0以外ならば1(TRUE)を返す関数を、三種類の書き方で書いてみました。 │ int a1(int x) { │ return !!x; │ } │ int a2(int x) { │ return x ? 1 : 0; │ } │ int a3(int x) { │ if(x) { │ return 1; │ } else { │ return 0; │ } │ } 最適化有り(-O2)でコンパイルすると、以下のアセンブラソースが生成されます。 │ a1: │ not %r10, %r12 │ add %r10, 1 │ or %r10, %r12 │ xsrl %r10, 31 │ ret │ a2: │ not %r10, %r12 │ add %r10, 1 │ or %r10, %r12 │ xsrl %r10, 31 │ ret │ a3: │ not %r10, %r12 │ add %r10, 1 │ or %r10, %r12 │ xsrl %r10, 31 │ ret 三種類の書き方の全てで、同じアセンブラソースが生成されています。 ぱっと見て、元の三種類のどれとも違う処理を行っているように見えます。 こうなる理由は、コンパイラの最適化によって、整数型を論理型に変換する処理が、以下のような処理方法に変えられているからです。 │ return (unsigned)(-x | x) >> 31; 多くのCPUでは条件分岐が遅いので、条件分岐を避けるために、整数型を論理型に変換する処理を上記のように変えるのが、コンパイラの定石だそうです。 一見、問題無いように見えますが、実は少し、問題が有ります。 上記のアセンプラソースは、実際の機械語に展開されると、こうなるからです。 │ not %r10, %r12 │ add %r10, 1 │ or %r10, %r12 │ srl %r10, 8 ;//┐ │ srl %r10, 8 ;//│xsrl %r10, 31 │ srl %r10, 8 ;//│ │ srl %r10, 7 ;//┘ │ ret 整数型を論理型に変換するだけの処理なのに、(retを除いて)7命令にもなってしまって、無駄が生じています。 実行時間は7サイクルです。 命令数が多くなる原因は、P/ECEのCPU S1C33209は、シフト・ローテート命令の機能が弱くて、31ビットのシフトを行うために4命令必要だからです。 ■原因の推測 P/ECE開発環境のCコンパイラは、MIPS CPU用のCコンパイラをベースに作成されたものだそうです。 MIPSは条件分岐が遅くて(※注)、シフト命令は遅くないので、前述のコンパイラの定石が適用されているのだと思います。 S1C33209用のCコンパイラにする時に、MIPS用の最適化処理がそのまま残っていて、却って無駄なコードが生成されるようになったのではないかと思います。 (※注) 僕はMIPS CPUでプログラムを作った事が無いので、実感としては良く判らないのですが... ■アセンブラで書く場合 実際のところ、S1C33209は条件分岐が遅くないので、以下のように素直に書く方が、却って効率が良いです。 (retを除いて)4命令、実行時間は(x==0)の時3サイクル,(x!=0)の時4サイクルです。 │ cmp %r12, 0 │ jreq.d 3 │ ld.w %r10, 0 │ ld.w %r10, 1 │ ret さらに工夫すると、以下のように書けます。 (retを除いて)3命令、実行時間は常に3サイクルです。 │ ld.w %r10, 0 │ cmp %r10, %r12 │ ret.d │ adc %r10, %r10 ;//ちなみにTRUEを1で表す場合はこの通りですが、VisualBasicみたいにTRUEを-1で表したい場合は、この行を「sbc %r10,%r10」に変えればokです。 ■C言語で書く場合2 アセンブラで書くならば上記のように工夫できるのですが、いつも全てアセンブラで書くわけには行きません。 C言語で書いて、余計な最適化(悪化?)を避ける方法を、検討してみました。 整数型を論理型に変換した結果を、一旦、他の変数に入れてみると… │ int b1(int x) { │ int y = !!x; │ return y; │ } │ int b2(int x) { │ int y = x ? 1 : 0; │ return y; │ } │ int b3(int x) { │ int y; │ if(x) { │ y = 1; │ } else { │ y = 0; │ } │ return y; │ } 以下のアセンブラソースが生成されました。 │ b1: │ ld.w %r10, %r12 │ cmp %r10, 0 │ jreq 2 │ ld.w %r10, 1 │ ret │ b2: │ ld.w %r10, %r12 │ cmp %r10, 0 │ jreq 2 │ ld.w %r10, 1 │ ret │ b3: │ not %r10, %r12 │ add %r10, 1 │ or %r10, %r12 │ xsrl %r10, 31 │ ret b1とb2は、概ね素直なコードが生成されるようになりました。 整数型を論理型に変換した結果を、一旦、他の変数に入れてみると良いみたいです。 しかし、b3はまだ、余計な最適化が掛かったままです。 そこで今度は、代入する変数を、元の変数自身に代入してみました。 │ int c1(int x) { │ x = !!x; │ return x; │ } │ int c2(int x) { │ x = x ? 1 : 0; │ return x; │ } │ int c3(int x) { │ if(x) { │ x = 1; │ } else { │ x = 0; │ } │ return x; │ } 以下のアセンブラソースが生成されました。 │ c1: │ ld.w %r10, %r12 │ cmp %r10, 0 │ jreq 2 │ ld.w %r10, 1 │ ret │ c2: │ ld.w %r10, %r12 │ cmp %r10, 0 │ jreq 2 │ ld.w %r10, 1 │ ret │ c3: │ ld.w %r10, %r12 │ cmp %r10, 0 │ jreq 2 │ ld.w %r10, 1 │ ret 全ての関数が、概ね素直なコードが生成されるようになりました。 整数型を論理型に変換した結果を、元の変数自身に代入すると良いみたいです。 ■C言語で書く場合3 ちなみに、変換結果をすぐに返すのでなく、変換結果を使って別の関数を呼び出す場合も、だいたい同じ傾向になるようです。 │ extern void foo(int x); │ //対策前 │ void d1(int x) { │ foo(!!x); │ } │ void d2(int x) { │ foo(x ? 1 : 0); │ } │ void d3(int x) { │ if(x) { │ foo(1); │ } else { │ foo(0); │ } │ } │ //対策後 │ void e1(int x) { │ x = !!x; │ foo(x); │ } │ void e2(int x) { │ x = x ? 1 : 0; │ foo(x); │ } │ void e3(int x) { │ if(x) { │ x = 1; │ } else { │ x = 0; │ } │ foo(x); │ } 以下のアセンブラソースが生成されました。 d3だけ、前の例と傾向が違いました。 変換結果を使って別の関数を呼び出す場合は、ifで分岐する方法でも、無駄な最適化が行われないようです。 │ ;//対策前 │ d1: │ not %r10, %r12 │ add %r10, 1 │ or %r10, %r12 │ ld.w %r12, %r10 │ xsrl %r12, 31 │ xcall foo │ ret │ d2: │ not %r10, %r12 │ add %r10, 1 │ or %r10, %r12 │ ld.w %r12, %r10 │ xsrl %r12, 31 │ xcall foo │ ret │ d3: │ cmp %r12, 0 │ jreq 2 │ ld.w %r12, 1 │ xcall foo │ ret │ ;//対策後 │ e1: │ cmp %r12, 0 │ jreq 2 │ ld.w %r12, 1 │ xcall foo │ ret │ e2: │ cmp %r12, 0 │ jreq 2 │ ld.w %r12, 1 │ xcall foo │ ret │ e3: │ cmp %r12, 0 │ jreq 2 │ ld.w %r12, 1 │ xcall foo │ ret アセンブラで書くならこんな感じ↓かな? Cコンパイラが生成したコードよりも、1サイクル高速です。 (この例では関数呼び出しをjp.dに置き換える事も出来ますが、今回の本題ではないのでそのままにしました) │ cmp %r12, 1 ;//%psr(C) := x ? 0 : 1 │ sbc %r12, %r12 ;//%r12 := x ? 0 : -1 │ xcall.d foo │ add %r12, 1 ;//%r12 := x ? 1 : 0 │ ret ■まとめ P/ECEのC言語で、整数型を論理型に変換する処理を書く場合は、整数型を論理型に変換した結果を元の変数自身に代入するようにすると、効率の良いコードが生成される期待が持てます。(まだ検証不足ですが…) もっとも、元の変数を破壊出来ない場合は無理ですし、そうしなくても少し効率の悪いコードが生成されるだけで結果には問題ないので、必須ではありません。 * Wed Oct 28 21:41:29 JST 2015 Naoyuki Sawa - pp33.exeのバグ(2) 前回に引き続き、P/ECE開発環境のpp33.exeのバグの話です。 前回のとは別のバグです。 以下のようなアセンブラソースを書いて、 │ xld.w %r0,symbol+0x80000000 コンパイルすると、エラーが出ます。 >pcc33.exe -c sample1.s □実行結果 │sample1.ps(1): Error: Invalid syntax. なぜエラーが出るかと言うと、コンパイル処理の途中で、pp33.exeが上記の行を以下のように変換してしまい、 │ xld.w %r0,symbol+-2147483648 コンパイル処理でpp33.exeの次に実行されるext33.exeが、「+-」という部分を処理出来ずにエラーなるからです。 一応、pp33.exeのマニュアルには、以下のように書いてあります。 │「S5U1C33000C Manual」(\usr\PIECE\docs\datasheet\EPSON\S5U1C33000C_J.pdf) p.117 │『9 プリプロセッサ』⇒『9.6 数値演算子』 │・内部的な演算は符号付き32ビットとして行われますので、演算の種類によっては注意が必要です。 │・演算結果が負の場合はマイナス符号付きの10進数で、正の場合は16進数で出力されます。 pp33.exeが、「symbol+0x80000000」の「+0x80000000」の「0x80000000」の部分を符号付き32ビットと見なして、 「0x80000000」=「-2147483648」なのでマイナス符号付きの10進数で出力し、「+-2147483648」になるわけです。 確かにマニュアルどおりの挙動なのですが、仕様バグですよね・・・ ちなみに上記は「+-」になってエラーが出る場合でしたが、「--」になってエラーが出る事も有ります。 │ xld.w %r0,symbol-0x80000000 □変換結果 │ xld.w %r0,symbol--2147483648 ところで、だいたいそもそも、 │ xld.w %r0,symbol+0x80000000 のようなプログラムを書く事が有るのかという話なのですが・・・有り得ます。たとえば、P/ECEカーネルの、 □\usr\PIECE\sysdev\pcekn\font.c │static const unsigned char __nofont[] = { │ 〜(略) │}; │#define NOFONT ((unsigned char *)__nofont) │static const unsigned char *_pceFontGetAdrs( unsigned short code ) │ 〜(略)〜 │ return NOFONT+0x80000000; ←←←←←ここ │ 〜(略)〜 │} の部分が、「xld.w %r0,symbol+0x80000000」と同様の処理です。 P/ECEのCPU S1C33のアドレス空間は28ビットなので、CPUが使わない上位4ビット分をフラグに利用しているのですね。 P/ECEカーネルの上記の部分では、なぜ、前述のバグによるエラーが出ないかと言うと、C言語プログラムだからです。 C言語プログラムの場合、以下の順でコンパイルされます。 (*.c) ⇒ gcc33.exe ⇒ (*.ps) ⇒ ext33.exe ⇒ (*.ms) ⇒ as33.exe ⇒ (*.o) C言語プログラムの場合は、途中で生成されるアセンブラソースがpp33.exeを通らないので、問題が生じなかったわけです。 一方、直接アセンブラプログラムを書いた場合は、以下のように処理されて、pp33.exeとext33.exeの部分で問題が出ます。 (*.s) ⇒ pp33.exe ⇒ (*.ps) ⇒ ext33.exe ⇒ (*.ms) ⇒ as33.exe ⇒ (*.o) なお、上記の処理順序はP/ECE開発環境のコンパイラドライバpcc33.exeが制御しているのですが、P/ECE開発環境特有の問題というわけではありません。 EPSONのCPUマニュアルに定義されている手順どおりだからです。 │「S5U1C33000C Manual」(\usr\PIECE\docs\datasheet\EPSON\S5U1C33000C_J.pdf) p.6 │『3 ソフトウェア開発手順』⇒『3.1 ソフトウェア開発フロー』 http://www.piece-me.org/piece-lab/pp33bug/pp33bug-20151028.png ■回避策 pp33.exeの出力ファイルを、sed等のツールでフィルタして、「+-」を「-」に,「--」を「+」に置換すれば回避できます。 厳密には、文字列の中でないかとか考慮する必要があるのですけれど、たいていの場合は単純な置換で大丈夫だと思います。 >onigsed.exe -i~ -e "s/+-/-/g" -e "s/--/+/g" sample1.ps * Tue Oct 27 21:58:28 JST 2015 Naoyuki Sawa - pp33.exeのバグ(1) P/ECE開発環境のpp33.exeには、バグがあります。 pp33.exeは、アセンブラソースファイル(*.s)をアセンブラ(as33.exe)に掛ける前に式やマクロの展開を行うツールで、いわゆるアセンブラプリプロセッサです。 pp33.exeには、一行255文字までしか読み込めないという制限が有ります。 文字数には、コメント部分や空白文字や、行末の改行文字も含みます。 特に「行末の改行文字も含む」と言う点が重要で、見た目に255文字でも、行末の改行文字を含めてちょうど256文字だとエラーが出ます。 □入力行 │ xld.w %r0,1234567890 ;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij □実行結果 │Error: Cannot read file. Line size is too long. 要するに、見た目には一行254文字までしか読み込めません。 □入力行 │ xld.w %r0,1234567890 ;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi □実行結果 │エラー無し。 上記の制限は、マニュアルに書いてあります。 │「S5U1C33000C Manual」(\usr\PIECE\docs\datasheet\EPSON\S5U1C33000C_J.pdf) p.122 │⇒『9 プリプロセッサ』⇒『9.12 エラー/ワーニングメッセージ』⇒『9.12.1 エラー』 │Error: Cannot read file. Line size is too long. │ステートメントが長すぎて読み込めません。各行で読み込み可能な文字数は最大255文字です。 しかし実は、その他に、マニュアルに書かれていない制限(バグ?)が有ります。 入力行だけでなく、出力行も、一行255文字までしか扱えないという制限です。 たとえば以下の行は行末の改行文字を含めて255文字で、正常に処理できるはずなのですが、実際にはpp33.exeに読み込ませると内部エラーが発生します。 式を展開した結果、出力行が一行255文字を超えてしまうからです。 □入力行 │ xld.w %r0,1<<30 ;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmn □実行結果 │Warning : internal strcpy over. │Warning : internal strcpy over. │Warning : internal strcpy over. │Warning : internal strcpy over. │Warning : internal strcpy over. │Warning : internal strcpy over. □出力結果 │ xld.w %r0,0x40000000 ;0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghmn 出力結果を見ると、一見正しく変換されているように見えますが、よく見ると最後の方の文字が途中で4文字分('ijkl')欠けています。 おそらく、pp33.exeの内部ではバッファオーバーランが発生して、予測できない結果になっているのだと思います。 一行しか入力していないのに6回も警告メッセージが出ている理由も、pp33.exeの内部で予測できない動作になっているからだと思います。 なお、上記の例では警告メッセージが出ていますが、バッファオーバーランの内容によっては、警告メッセージが出ずに黙って不正な出力結果になる事も有るようです。 そうなると、気付かずに意図しないコンパイル結果になってしまう可能性があり、非常に危険です。 まあ普通は、そんなに長い行の末尾はコメント部分だと思うので、コメントの末尾が欠けても動作に影響は無いとは思いますが… とはいえ「予測できない動作」ですので、末尾が欠けるだけとは限らず、何が起きるか判らないので、やはり危険です。 ■結論 pp33.exeの入力ファイルは、一行の文字数を255文字よりも「余裕を持って」少なめにしておく必要が有ります。 どれぐらい少なければ安全かは展開する内容次第なのですけれど、200文字程度ならば大抵は安全だと思います。 * Tue Sep 29 21:28:31 JST 2015 Naoyuki Sawa - srf33オブジェクトファイルの構造のマニュアル間違い P/ECE開発環境のコンパイラマニュアル「S5U1C33000C Manual」(\usr\PIECE\docs\datasheet\EPSON\S5U1C33000C_J.pdf)の、 「Appendix srf33ファイルの構造 A-1 srf33オブジェクトファイルの構造」(p.475〜479)には、間違いが有るようです。 p.478の「(4)エクスターン情報」の表の、e_scnndxが『4Byte』と記載されていますが、実際には『2Byte』だと思います。 サンプルデータを使って検証してみました。 │ .code │ .global Test1to9 │ Test1to9: │ .byte 1 │ .byte 2 │ .byte 3 │ .byte 4 │ .byte 5 │ .byte 6 │ .byte 7 │ .byte 8 │ .byte 9 上記のアセンブラソースをコンパイルして、sample.oを生成しました。 │ pcc33 -c sample.s sample.oをバイナリエディタで開いて、「A-1 srf33オブジェクトファイルの構造」と見比べて、コメントを付けました。 │ //srf33制御ヘッダ │ 00 01 //c_fatt 2Byte ファイル制御フラグ │ 00 00 //c_pentry 2Byte エントリーアドレス │ 33 00 //c_ver 2Byte srf33バージョン情報 │ 00 03 //c_scncnt 2Byte セクション情報数 │ 00 00 00 10 //c_scnptr 4Byte セクション情報チェーン │ 00 00 00 00 //c_debptr 4Byte デバッグ制御情報チェーン │ //セクション情報(CODE) │ 00 00 00 3C //s_nxptr 4Byte 次のセクション情報へのチェーン │ 00 01 //s_scntyp 2Byte セクションタイプ │ 00 00 //s_lnktyp 2Byte リンク方法 │ 00 01 //s_scnatt 2Byte セクション属性 │ 00 00 00 00 //s_off 4Byte セクションのスタートアドレス │ 00 00 00 00 //s_rcptr 4Byte リロケーション情報チェーン │ 00 00 00 00 //s_rcsiz 4Byte リロケーション情報バイトサイズ │ 00 00 00 94 //s_exptr 4Byte エクスターン情報チェーン │ 00 00 00 15 //s_exsiz 4Byte エクスターン情報バイトサイズ │ 00 00 00 01 //s_excnt 4Byte エクスターン情報の個数 │ 00 00 00 A9 //s_rdptr 4Byte 実データへのチェーン │ 00 00 00 09 //s_dsiz 4Byte 実データバイトサイズ │ 00 00 //s_scnndx 2Byte セクションID │ //セクション情報(DATA) │ 00 00 00 68 //s_nxptr 4Byte 次のセクション情報へのチェーン │ 00 02 //s_scntyp 2Byte セクションタイプ │ 00 00 //s_lnktyp 2Byte リンク方法 │ 00 01 //s_scnatt 2Byte セクション属性 │ 00 00 00 00 //s_off 4Byte セクションのスタートアドレス │ 00 00 00 00 //s_rcptr 4Byte リロケーション情報チェーン │ 00 00 00 00 //s_rcsiz 4Byte リロケーション情報バイトサイズ │ 00 00 00 00 //s_exptr 4Byte エクスターン情報チェーン │ 00 00 00 00 //s_exsiz 4Byte エクスターン情報バイトサイズ │ 00 00 00 00 //s_excnt 4Byte エクスターン情報の個数 │ 00 00 00 00 //s_rdptr 4Byte 実データへのチェーン │ 00 00 00 00 //s_dsiz 4Byte 実データバイトサイズ │ 00 01 //s_scnndx 2Byte セクションID │ //セクション情報(BSS) │ 00 00 00 00 //s_nxptr 4Byte 次のセクション情報へのチェーン │ 00 03 //s_scntyp 2Byte セクションタイプ │ 00 00 //s_lnktyp 2Byte リンク方法 │ 00 01 //s_scnatt 2Byte セクション属性 │ 00 00 00 00 //s_off 4Byte セクションのスタートアドレス │ 00 00 00 00 //s_rcptr 4Byte リロケーション情報チェーン │ 00 00 00 00 //s_rcsiz 4Byte リロケーション情報バイトサイズ │ 00 00 00 00 //s_exptr 4Byte エクスターン情報チェーン │ 00 00 00 00 //s_exsiz 4Byte エクスターン情報バイトサイズ │ 00 00 00 00 //s_excnt 4Byte エクスターン情報の個数 │ 00 00 00 00 //s_rdptr 4Byte 実データへのチェーン │ 00 00 00 00 //s_dsiz 4Byte 実データバイトサイズ │ 00 02 //s_scnndx 2Byte セクションID │ //エクスターン情報 │ 00 00 00 00 //e_scnoff 4Byte セクション内のオフセット │ 00 00 00 00 //e_size 4Byte シンボルのサイズ │ 00 00 //e_scnndx 4Byte 参照するエクスターン情報が所属するセクションのID ←←←『4Byte』が間違い │ 00 01 //e_extyp 2Byte エクスターンタイプ │ 08 //e_namsiz 1Byte シンボル名の長さ │ 54 65 73 74 31 74 6F 39 //e_exnam *Byte シンボル名 │ //実データ │ 01 02 03 04 05 06 07 08 09 // エクスターン情報のe_scnndxを、4Byteでなく2Byteと見なさないと、合いません。 e_scnndxはセクションのIDを表すフィールドで、他のセクションIDフィールド(s_scnndx)は2Byteですから、e_scnndxも2Byteなのが自然です。 というわけで、エクスターン情報のe_scnndxは、『2Byte』が正しいと思います。 http://www.piece-me.org/piece-lab/srf33/e_scnndx-fix.jpg * Sun Apr 05 21:06:45 JST 2015 Naoyuki Sawa - gcc33 abs()の最適化バグ P/ECE開発環境のCコンパイラgcc33には、abs()の最適化バグがあります。 abs()に対して、マイナスの値を指定した時にマイナスのまま返されたり、プラスの値を指定した時にマイナスになって返されたりするバグです。 ■バグを再現するプログラム 以下に、バグを再現するサンプルプログラムの例を示します。 最適化オプション「-O2」を指定してコンパイルすると、最適化バグが発生します。 この例の場合、「マイナスの値を指定した時にマイナスのまま返される」バグが発生しています。 □absbug1/absbug1.c │ │ #include │ #include │ //------------------------------------------------------------------------------ │ unsigned char vbuff[DISP_X * DISP_Y]; │ //{{--- テスト --- │ int x, y = 1; │ void test(); │ //}}--- テスト --- │ //------------------------------------------------------------------------------ │ void pceAppInit() { │ pceAppSetProcPeriod(1); │ pceLCDSetBuffer(vbuff); │ pceLCDDispStart(); │ } │ //------------------------------------------------------------------------------ │ void pceAppProc(int cnt) { │ memset(vbuff, 0, sizeof vbuff); │ pceFontSetPos(0, 0); │ pceFontSetTxColor(3); │ pceFontSetBkColor(0); │ //{{--- テスト --- │ x = 5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 10」(正しい) │ x = -5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = -10」(正しい) │ x = 10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 0」(正しい) │ x = -10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = -20」(誤り) │ //}}--- テスト --- │ pceLCDTrans(); │ //SELECTボタンが押されたら終了する。 │ if(pcePadGet() & TRG_SELECT) { pceAppReqExit(0); } │ } │ //------------------------------------------------------------------------------ │ void pceAppExit() { │ } │ //------------------------------------------------------------------------------ │ //{{--- テスト --- │ void test() { │ //変数xの値を2倍にする。 │ x <<= 1; │ //変数yの値が0でなければ… │ if(y) { │ //変数xの絶対値が15以上ならば… │ if(abs(x) >= 15) { │ //変数xの値を0に戻す。 │ x = 0; │ } │ } │ } │ //}}--- テスト --- │ ■バグの原因を調査する gcc33がtest()関数をコンパイルして生成した、アセンブラコードを示します。(少し整形しました) □absbug1/absbug1.ps │ │ test: │ xld.w %r10, [x] ;//%r10 := x │ ld.w %r11, %r10 ;//%r11 := x │ sll %r11, 1 ;//%r11 := x <<= 1 │ xld.w [x], %r11 ;//xをメモリに格納する。 │ xld.w %r10, [y] ;//%r10 := y │ cmp %r10, 0 ;//if(y) { │ jreq __L2 ;// │ ld.w %r10, %r11 ;// %r10 := x ┐ │ jrge __L1 ;// if(???) { ←───────│───────────ここがバグ!! │ not %r10, %r10 ;// %r10 := ~x ├この範囲がabs(x)相当 │ add %r10, 1 ;// %r10 := ~x + 1 = -x │ │ __L1: ;// } ┘ │ cmp %r10, 14 ;// if(abs(x) > 14) │ jrle __L2 ;// │ ld.w %r12, 0 ;// %r12 := x = 0 │ xld.w [x], %r12 ;// xをメモリに格納する。 │ __L2: ;//} } │ ret ;// │ abs()の処理で、xが0以上かを判定するための「ld.w %r10,0」が抜けている事が、バグの原因です。 abs()の引数'x'を比較せず、直前の処理の比較結果'y'によって、abs()の符号反転を行ってしまっているので、abs()が間違った結果になります。 ■バグが発生する条件(推測) いろいろなパターンをためしたところ、このバグが発生する条件は、おおよそ以下のようです。(推測です) �� abs()の引数を、関数の最初の方で既に使用していて、レジスタにロード済みである。 �� abs()の直前で、abs()の引数以外の変数を、0と比較する処理を行っている。 上のサンプルプログラムの場合、test()の中の「x <<= 1;」が�,冒蠹�し、「if(y) {」が�△冒蠹�します。 どうやらgcc33は、「abs()の引数を0と比較する処理」を、�△糧羈喀萢�と(間違って)まとめて最適化してしまうバグがあるようです。 gcc33の内部処理的には、2013年7月30日のP/ECE研究記録、「gcc33 条件演算子の最適化バグ」で調査した件と、同じではないかと思います。 abs()が組み込み関数としてインライン展開された後、「gcc33 条件演算子の最適化バグ」と同じ、間違った最適化が行われているのだと思います。 ■バグを回避する方法 gcc33がabs()を組み込み関数としてインライン展開しないように、マクロ定義で置き換える事にしました。 どのように置き換えると正しい動作になるか・効率の良いコードになるかを、検証して見ました。 ×正しくない置き換え まず、素直な比較処理で置き換えてみました。 □absbug2/absbug2.c │ │ #define abs(x) ({ \ │ int __x__ = (x); \ │ if(__x__ < 0) { __x__ = -__x__; } \ │ __x__; }) │ しかしこれでは、abs()の結果が間違ったままになりました。 │ │ x = 5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 10」(正しい) │ x = -5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = -10」(正しい) │ x = 10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 0」(正しい) │ x = -10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = -20」(誤り) │ アセンブラコードを見て見ると: □absbug2/absbug2.ps │ │ test: │ xld.w %r10, [x] ;//%r10 := x │ ld.w %r11, %r10 ;//%r11 := x │ sll %r11, 1 ;//%r11 := x <<= 1 │ xld.w [x], %r11 ;//xをメモリに格納する。 │ xld.w %r10, [y] ;//%r10 := y │ cmp %r10, 0 ;//if(y) { │ jreq __L2 ;// │ ld.w %r10, %r11 ;// %r10 := x ┐ │ jrge __L1 ;// if(???) { ←───────│───────────バグのまま!! │ not %r10, %r10 ;// %r10 := ~x ├この範囲がabs(x)相当 │ add %r10, 1 ;// %r10 := ~x + 1 = -x │ │ __L1: ;// } ┘ │ cmp %r10, 14 ;// if(abs(x) > 14) │ jrle __L2 ;// │ ld.w %r12, 0 ;// %r12 := x = 0 │ xld.w [x], %r12 ;// xをメモリに格納する。 │ __L2: ;//} } │ ret ;// │ 最初のサンプルプログラムと、全く同じアセンブラコードが生成されています。 素直な比較処理では、「gcc33 条件演算子の最適化バグ」と同じ最適化バグが生じて、バグ回避にならないようです。 ※ちなみにこの結果から推測すると、2つの変数に対して0との比較処理を連続して行うと、abs()以外でもバグが顕在化する可能性がありそうです。 ※今回の本題からは外れるますが、結構怖いです・・・今後、要調査です。 ×正しくない置き換え 次に、比較演算子を使って置き換えて見ました。 □absbug3/absbug3.c │ │ #define abs(x) ({ \ │ int __x__ = (x); \ │ (__x__ >= 0) ? x : -__x__; }) │ しかしこれでも、abs()の結果が間違ったままになりました。 □absbug3/absbug3.ps │ │ test: │ xld.w %r10, [x] ;//%r10 := x │ ld.w %r11, %r10 ;//%r11 := x │ sll %r11, 1 ;//%r11 := x <<= 1 │ xld.w [x], %r11 ;//xをメモリに格納する。 │ xld.w %r10, [y] ;//%r10 := y │ cmp %r10, 0 ;//if(y) { │ jreq __L2 ;// │ ld.w %r10, %r11 ;// %r10 := x ┐ │ jrge __L1 ;// if(???) { ←───────│───────────バグのまま!! │ not %r10, %r10 ;// %r10 := ~x ├この範囲がabs(x)相当 │ add %r10, 1 ;// %r10 := ~x + 1 = -x │ │ __L1: ;// } ┘ │ cmp %r10, 14 ;// if(abs(x) > 14) │ jrle __L2 ;// │ ld.w %r12, 0 ;// %r12 := x = 0 │ xld.w [x], %r12 ;// xをメモリに格納する。 │ __L2: ;//} } │ ret ;// │ 最初のサンプルプログラムと、全く同じアセンブラコードが生成されています。 というわけで、この方法も×です。 ×正しくない置き換え 次に、比較演算子で、GCC拡張機能を使って置き換えて見ました。 □absbug4/absbug4.c │ │ #define abs(x) ({ \ │ int __x__ = (x); \ │ (__x__ >= 0) ? : -__x__; }) │ 2013年7月30日の「gcc33 条件演算子の最適化バグ」の時は、GCC拡張機能を使うとバグが回避できたので、この方法で大丈夫かと思ったのですが、 しかし試してみると、abs()の結果が間違った結果になりました。 │ │ x = 5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 10」(正しい) │ x = -5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = -10」(正しい) │ x = 10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 20」(誤り) │ x = -10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 0」(正しい) │ 先ほどまでは、マイナス値に対するabs()が間違っていてプラス値に対するabs()は正しかったのですが、今回は逆になっています。 アセンブラコードを見て見ると: □absbug4/absbug4.ps │ │ test: │ xld.w %r10, [x] ;//%r10 := x │ ld.w %r11, %r10 ;//%r11 := x │ sll %r11, 1 ;//%r11 := x <<= 1 │ xld.w [x], %r11 ;//xをメモリに格納する。 │ xld.w %r10, [y] ;//%r10 := y │ cmp %r10, 0 ;//if(y) { │ jreq __L2 ;// │ not %r10, %r11 ;// %r10 := ~x │ xsrl %r10, 31 ;// %r10 := ~x >> 31 xがプラスならば1,xがマイナスならば0になる。 ←0と比較して判定すれば1命令で済むのに、4命令使って最上位ビットをシフトするという無駄!! │ jrne __L1 ;// if(!(~x >> 31)) { ─┐ │ not %r10, %r11 ;// %r10 := ~x  │ifの中を通った時(xがマイナスの時)は、%r10=abs(x)になるが、 │ add %r10, 1 ;// %r10 := ~x + 1 = -x  │ifの中を通らなかった時(xがプラスの時)は、%r10=(~x>>31)になる。 ←あらたなバグ!! │ __L1: ;// } ←┘ │ cmp %r10, 14 ;// if(??? > 14) { │ jrle __L2 ;// │ ld.w %r12, 0 ;// %r12 := x = 0 │ xld.w [x], %r12 ;// xをメモリに格納する。 │ __L2: ;//} } │ ret ;// │ なんかとんでもないコードが生成されました。効率が悪い上に結果も間違っています。 この方法も×です。 ※なぜこんなコードが生成されるのでしょうか? ※今回の本題からは外れるますが、結構怖いです・・・今後、要調査です。 ○正しい置き換え C言語で書くと正しいコードが生成されなさそうな事が判ったので、アセンブラで置き換えて見る事にしました。 □absbug5/absbug5.c │ │ #define abs(x) ({ \ │ int __x__ = (x); \ │ asm /*volatile*/ ( \ │ "cmp %0, 0 \n" \ │ "jrge 3 \n" \ │ " not %0, %0 \n" \ │ " add %0, 1 \n" \ │ : "=r"(__x__) : "0"(__x__) : "cc"); \ │ __x__; }) │ 以下のとおり、正しい結果になりました。 │ │ x = 5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 10」(正しい) │ x = -5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = -10」(正しい) │ x = 10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 0」(正しい) │ x = -10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 0」(正しい) │ アセンブラコードは、こうなります。 │ │ test: │ xld.w %r10, [x] ;//%r10 := x │ ld.w %r11, %r10 ;//%r11 := x │ sll %r11, 1 ;//%r11 := x <<= 1 │ xld.w [x], %r11 ;//xをメモリに格納する。 │ xld.w %r10, [y] ;//%r10 := y │ cmp %r10, 0 ;//if(y) { │ jreq __L1 ;// │ ld.w %r10, %r11 ;// %r10 := x │ cmp %r10, 0 ;// if(x < 0) { ┐ │ jrge 3 ;// ├アセンブラで置き換えた範囲 │ not %r10, %r10 ;// %r10 := ~x │ │ add %r10, 1 ;// %r10 := ~x + 1 = -x } ┘ │ cmp %r10, 14 ;// if(abs(x) > 14) { │ jrle __L1 ;// │ ld.w %r12, 0 ;// %r12 := x = 0 │ xld.w [x], %r12 ;// xをメモリに格納する。 │ __L1: ;//} │ ret │ 自然なコードが生成されています。 厳密に言えば、Cコンパイラが生成した部分のレジスタの使用方法に無駄がありますが、まあ置いておく事にして。 というわけで、この方法で「gcc33 abs()の最適化バグ」が回避できる事が判りました。 結論から言うと、これが最善の方法です。 以下、別の方法も検証してみましたが、これよりも効率の悪い方法なので、見なくても構いません。 △正しい置き換え(別ver)。結果は正しいけれど効率が悪い。 「標準Cライブラリの実装」(http://libc.blog47.fc2.com/)さんの「abs関数」(http://libc.blog47.fc2.com/blog-entry-57.html)の記事を参照させて頂きました。 それによると、RISCのように分岐のコストが大きいプロセッサでは、分岐を使わずにabs()を実装する事があるそうです。 │ │ int t = x >> 31; │ return (x ^ t) - t; │ ただし、分岐よりもシフト演算の方がコストが大きいマイコン(H8など)では、逆効果になる事もあるそうです。 P/ECEのCPU S1C33209は、RISCマイコンなのですけれど、分岐よりもシフト演算の方がコストが大きいと思うので、逆効果になりそうです。 一応、試して見る事にしました。 □absbug6/absbug6.c │ │ #define abs(x) ({ \ │ int __x__ = (x); \ │ asm /*volatile*/ ( \ │ "ld.w %%r9, %0 \n" \ │ "xsra %%r9, 31 \n" \ ←──4命令に展開されます。 │ "xor %0, %%r9 \n" \ │ "sub %0, %%r9 \n" \ │ : "=r"(__x__) : "0"(__x__) : "cc","%r9"); \ │ __x__; }) │ 実行結果は、こうなりました。 │ │ x = 5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 10」(正しい) │ x = -5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = -10」(正しい) │ x = 10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 0」(正しい) │ x = -10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 0」(正しい) │ アセンブラコードは、こうなりました。 □absbug6/absbug6.ps │ │ test: │ xld.w %r10, [x] ;//%r10 := x │ ld.w %r11, %r10 ;//%r11 := x │ sll %r11, 1 ;//%r11 := x <<= 1 │ xld.w [x], %r11 ;//xをメモリに格納する。 │ xld.w %r10, [y] ;//%r10 := y │ cmp %r10, 0 ;//if(y) { │ jreq __L1 ;// │ ld.w %r10, %r11 ;// %r10 := x │ ld.w %r9, %r10 ;// %r9 := x ┐ │ sra %r9, 8 ;// %r9 := x >> 8 │ │ sra %r9, 8 ;// %r9 := x >> 16 │ │ sra %r9, 8 ;// %r9 := x >> 24 ├アセンブラで置き換えた範囲 │ sra %r9, 7 ;// %r9 := t = x >> 31 │ │ xor %r10, %r9 ;// %r10 := x ^ t │ │ sub %r10, %r9 ;// %r10 := (x ^ t) - t = abs(x) ┘ │ cmp %r10, 14 ;// if(abs(x) > 14) { │ jrle __L1 ;// │ ld.w %r12, 0 ;// %r12 := x = 0 │ xld.w [x], %r12 ;// xをメモリに格納する。 │ __L1: ;//} │ ret │ 実行結果は正しいのですけれど、コードサイズも大きいし、実行速度も遅いです。 「分岐を使わずにabs()を実装する」方法は、S1C33209には向いていない事が判りました。 △正しい置き換え(別ver)。結果は正しいけれど効率が悪い。 「分岐を使わずにabs()を実装する」方法を少し修正して、シフト演算のコストを低減する事が出来ます。 □absbug7/absbug7.c │ │ #define abs(x) ({ \ │ int __x__ = (x); \ │ asm /*volatile*/ ( \ │ "swap %%r9, %0 \n" \ │ "ld.b %%r9, %%r9 \n" \ │ "sra %%r9, 7 \n" \ │ "xor %0, %%r9 \n" \ │ "sub %0, %%r9 \n" \ │ : "=r"(__x__) : "0"(__x__) : "cc","%r9"); \ │ __x__; }) │ 実行結果は、こうなりました。 │ │ x = 5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 10」(正しい) │ x = -5; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = -10」(正しい) │ x = 10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 0」(正しい) │ x = -10; │ test(); │ pceFontPrintf("x = %d\n", x); //⇒「x = 0」(正しい) │ アセンブラコードは、こうなりました。 □absbug7/absbug7.ps │ │ test: │ xld.w %r10, [x] ;//%r10 := x │ ld.w %r11, %r10 ;//%r11 := x │ xsll %r11, 1 ;//%r11 := x <<= 1 │ xld.w [x], %r11 ;//xをメモリに格納する。 │ xld.w %r10, [y] ;//%r10 := y │ cmp %r10, 0 ;//if(y) { │ jreq __L1 ;// │ ld.w %r10, %r11 ;// %r10 := x │ swap %r9, %r10 ;// %r9[7:0] := x[31:24] ┐ │ ld.b %r9, %r9 ;// %r9 := x[31:24] 符号拡張 │ │ sra %r9, 7 ;// %r9 := x[31] = t ├アセンブラで置き換えた範囲 │ xor %r10, %r9 ;// %r10 := x ^ t │ │ sub %r10, %r9 ;// %r10 := (x ^ t) - t = abs(x) ┘ │ cmp %r10, 14 ;// if(abs(x) > 14) { │ jrle __L1 ;// │ ld.w %r12, 0 ;// %r12 := x = 0 │ xld.w [x], %r12 ;// xをメモリに格納する。 │ __L1: ;//} │ ret │ 前のverよりは改善しましたが、それでも、分岐を使って実装したverよりはコードサイズが大きく、実行速度も遅いです。 やっぱり、「分岐を使わずにabs()を実装する」方法は、S1C33209には向いていない事が判りました。 ■まとめ P/ECE開発環境のCコンパイラgcc33の、abs()の最適化バグを回避する方法は、「absbug5/absbug5.c」の方法が最善です。 アプリケーションプログラムでabs()を使う事は、意外と少ないし、その上、absの最適化バグが発生する条件に一致する事はもっと少ないと思うのですが、 安全のために、「absbug5/absbug5.c」のマクロを、いつも定義しておくのが良いと思います。 ちなみに、今回は「abs()」についてだけ書きましたが、「labs()」についても全く同じです。 「absbug5/absbug5.c」のマクロに、labs()も追加して、以下のように定義するのが良いと思います。 │ │ #define abs(x) ({ \ │ int __x__ = (x); \ │ asm /*volatile*/ ( \ │ "cmp %0, 0 \n" \ │ "jrge 3 \n" \ │ " not %0, %0 \n" \ │ " add %0, 1 \n" \ │ : "=r"(__x__) : "0"(__x__) : "cc"); \ │ __x__; }) │ #define labs(x) abs(x) ←──これを追加 │ 今回の調査の中で、abs()の最適化バグとは別の、気になる点もいくつか発生しました。(absbug2/absbug2.cとabsbug3/absbug3.cの※の部分を参照) それらの点も、今後、調査して見ようと思います。 今回使用した検証プログラム一式の、ダウンロードはこちらです: http://www.piece-me.org/piece-lab/absbug/absbug-src.zip * Wed Jan 21 21:03:25 JST 2015 Naoyuki Sawa - P/ECEのsize_tはunsigned。unsigned longではない。 size_tの型は処理系依存です。大抵の32ビット環境では、unsigned,又は,unsigned longと定義されています。 unsignedでもunsigned longでも使用上の違いは無く、どちらであるかを気にする事はあまり無いです。 しかし、P/ECE開発環境を使っていて、unsignedとunsigned longの違いが問題になるケースが発生しました。 <例1> │#include │#include │extern int memcmp(const void* x, const void* y, size_t n); │int test1(const char* x, const char* y, size_t n) { │ return memcmp(x, y, n); │} <例2> │#include │#include │extern int memcmp(const void* x, const void* y, size_t n); │int test1(const char* x, const char* y, size_t n) { │ return memcmp(x, y, n); │} <例1>のソースをコンパイルすると以下の警告が出ます。 warning: conflicting types for built-in function `memcmp' 一方、<例2>のソースは、警告無くコンパイルできます。 <例1>と<例2>の違いは、stdio.hとstddef.hをインクルードする順番の違いだけです。 なぜ、<例1>では警告が出て、<例2>では警告が出ないのでしょうか。 stdio.hとstddef.hを比較したところ、size_tの定義が違っている事に気付きました。 □\usr\PIECE\include\stdio.h │〜 │#ifndef _SIZE_T │#define _SIZE_T │typedef unsigned long size_t; /* size of type */ │#endif │〜 □\usr\PIECE\include\stddef.h │〜 │#ifndef _SIZE_T │#define _SIZE_T │#ifdef __STDC__ │ typedef TYPEOF(sizeof(0)) size_t; │#else │ /* compiled with "-traditional" switch */ │ typedef unsigned size_t; │#endif │#endif │〜 stdio.hは、size_tをunsigned long型と定義しています。 stddef.hは、__STDC__によって場合分けされていますけれど、どちらにしても結果的に、size_tがunsigned型に定義されます。 マクロ_SIZE_Tによって、先にインクルードされた方の定義が有効になります。 従って、<例1>ではsize_tがunsigned long型、<例2>ではsize_tがunsigned型になっていたわけです。 size_tがunsigned long型の時に、 warning: conflicting types for built-in function `memcmp' の警告が出る理由は、P/ECE開発環境のCコンパイラでは、memcmp()がコンパイラのビルトイン関数だからです。 ヘッダファイルでsize_tがどのように定義されているかに関係なく、CコンパイラのEXEファイルの中で、 int memcmp(const void* x, const void* y, unsigned n); という関数形式に決め打ちになっています。<例1>は、ソースの中で、 int memcmp(const void* x, const void* y, unsigned long n); と宣言したことになって、コンパイラが決め打ちにしている関数形式と違っている、ということで警告が表示されるのでした。 P/ECEのプログラムを作っていて、上記の問題が顕在化することは、あまり無いです。 なぜなら、P/ECE開発環境のヘッダファイルで、memcmp()は以下のように宣言されているからです。 □\usr\PIECE\include\string.h │〜 │int memcmp( /* char *, char *, int */ ) ; │〜 引数を明示しない、K&R形式で宣言されています。 だから、size_tの定義がunsignedlongであっても影響は無く、警告は表示されないのです。 問題が顕在化するケースの一例としては、オープンソースのプログラムをコンパイルする時などです。 stddef.hよりも先にstdio.hをインクルードしていて、かつ、memcmp()をANSI形式で明示的に宣言していると、警告が出ます。 ちなみにmemcmp()だけでなく、memcpy()でも同様の警告が出ます。memcpy()もsize_t型の引数を持つ、ビルトイン関数だからです。 問題の原因は、P/ECE開発環境の標準インクルードファイルの、stddef.h以外がsize_tの定義が間違っている事です。 これまでの説明では、stdio.hを問題視していましたが、stdio.hだけでなく、stdlib.h,string.h,time.hも間違っています。 根本的に解決するには、これらのヘッダファイルを書き換える事なのですが、それはあまりやりたくないです。 代りの対策案を二つ考えてみました。 □対策案1 常に、stddef.hを最初にインクルードする。(でも、オープンソースをコンパイルする時とかは、変更点が多くて難しいかも…) □対策案2 コンパイラオプションで「-Dsize_t=unsigned -D_SIZE_T」と指定する。(Makefileの変更だけで済むから、こっちの方がいいかな…) ところで別の話ですが、P/ECE開発環境のstring.hは、size_tだけじゃなくてNULLの定義も間違っています。 本来、C言語では「#define NULL ((void*)0) なのですが、「#define NULL 0」になっていました。 今まで気付かなかったのですけれど、これも思わぬところで問題になりそうな気がします。 * Sat Jan 03 00:00:00 JST 2015 Naoyuki Sawa - USBのセレクティブサスペンドでP/ECEがハングアップする 「P/ECEのアプリが、2ミリ秒間以上割り込みを禁止すると、P/ECEがハングアップする可能性がある」という事に気付きました。 USBのセレクティブサスペンドにかかわる問題です。 ■検証を始める前に 検証を始める前に、注意点があります。 PC環境によって、セレクティブサスペンドが、有効だったり無効だったりする事です。 例えば、僕が使っている「Intel 965」のノートPCは、どう設定してもセレクティブサスペンドを有効にできませんでした。 別の「Intel 915 チップセット」のデスクトップPCは、デフォルト設定のままで、セレクティブサスペンドが有効でした。 Webで調べてみたところ、同時に使用している他のUSB機器や、デバイスドライバの種類によって影響されるそうです。 セレクティブサスペンドが無効なPCでは、今回検証する問題が再現しません。 ただし、実験のために無理やり再現する方法はあって、P/ECEに電池を入れてUSBケーブルを抜けば再現できます。 セレクティブサスペンドとは、デバイス側(=P/ECE側)から見ると、「SOFパケット」という通信が一定時間届かない事です。 デバイス側から見れば、PCがSOFパケットを送らないのも、USBケーブルを抜いて通信が途絶えるのも、同じ事に見えるからです。 というわけで、セレクティブサスペンドが無効なPCで実験する場合は、P/ECEに電池を入れてUSBケーブルを抜いて下さい。 尚、セレクティブサスペンドが有効もなPCでは、run.batでP/ECEのアプリを実行した後、USB通信が発生しなければ約5秒後に自動的にサスペンドします。(見た目ではわかりません) ■問題を再現するアプリ http://www.piece-me.org/piece-lab/usbsus/usbsus1.c まず、問題の現象を再現してみます。 usbsus1.srfを実行してください。 メインループの処理は、数字を増やしながら画面表示するだけの処理です。 Aボタンを押すと、割り込みを禁止して、5秒間待ちます。 5秒間経ったら、またメインループの処理に戻ります。 割り込み禁止になる5秒間のあいだを狙って、セレクティブサスペンドを発生させてください。 セレクティブサスペンドが有効もなPCならば、run.batでP/ECEのアプリを実行した後、3秒ぐらい待ってAボタンを押せば、ちょうど割り込み禁止中にセレクティブサスペンドすると思います。 セレクティブサスペンドが無効もなPCならば、Aボタンを押した後、5秒以内にUSBケーブルを抜いてください。 上記を行うと、5秒間経った後メインループの処理に戻らずに、P/ECEがハングアップします。 ハングアップした後に、正常復帰させてメインループを再開する方法は、一応、有ります。 セレクティブサスペンドが有効もなPCならば、何かUSB通信を発生させて、セレクティブサスペンドを解除すれば良いです。(例えば「isd.exe =」とタイプしてファイル一覧を取得する、など) セレクティブサスペンドが無効もなPCならば、USBケーブルをさせば、自動的にUSB通信が発生して正常復帰し、メインループが再開します。 とは言え、アプリがハングアップした時にユーザーがこの問題が原因だと気付いて、上記の操作を行ってもらう事は現実的ではありません。 ■原因の推測 割り込み禁止中にセレクティブサスペンドが発生するとハングアップする問題の、原因を調査しました。 いろいろ試したところ、USB割り込みが掛かりっ放しになっていることがわかりました。 USB割り込みが掛かりっ放しなので、メインループが回らなくなって、ハングアップしているように見えるのです。 具体的には、USB割り込みルーチンが呼び出されて、USB割り込みルーチンがUSB割り込みを解除する操作をしてリターンしたにもかかわらず、 実際にはUSB割り込みが解除されておらず、またUSB割り込みルーチンが呼び出される・・・という繰り返しです。 USB割り込みルーチンがUSB割り込みを解除する操作をしているのに、なぜ、USB割り込みが解除されないのかを、考えてみました。 通常のUSB通信割り込みでは問題が発生せず、'SUSPEND CHANGE'割り込み時のみ問題が発生することから推測して、 『USBコントローラ(PDIUSBD12)が、サスペンド状態に完全に移行してしまうと、CPU(S1C33209)からの一切の操作を受け付けなくなるのではないか』と推測しました。 具体的には、以下のような流れです。 USBコントローラがサスペンドへ移行する要因(=PCからのセレクティブサスペンド,又は,USBケーブルを抜いた)が発生して、USBコントーラがサスペンドへ移行を開始します。 まず、サスペンド状態が変わったことを、'SUSPEND CHANGE'割り込みによって、CPUへ知らせます。(USB SUSPEND信号⇒Hi、USB INT-N信号⇒Lo) その後、一定時間後に、USBコントローラは自身のクロックを止めて、完全にサスペンド状態になります。 USBコントローラが完全にサスペンド状態になる前に、CPUがUSBコントローラに対して、割り込み解除操作を行えば、USB INT-N信号⇒Hiになるのですが、 もしCPUの処理が遅れて、割り込み解除操作を行う前にUSBコントローラが完全にサスペンドしてしまうと、もう、USB INT-N信号をHiにする方法は有りません。 USB INT-N信号=Loのままになり、USB割り込みがかかりっぱなしになります。 USB通信を行ったりUSBケーブルをさしたりして、USBコントローラがサスペンドから復帰すれば、USBコントローラが割り込み解除操作を受け付けるようになり、正常復帰します。 USBコントローラがサスペンドした後に、サスペンドを解除する方法は、PC側からのUSB通信しか無く、CPU側からサスペンドを解除する手段は無いみたいです。 ■原因を検証するアプリ http://www.piece-me.org/piece-lab/usbsus/usbsus2.c 上記の推測が正しいかどうかを、試してみます。 usbsus2.srfを実行してください。 usbsus1.srfと同じ手順で操作してみると、今度は、ハングアップせずにメインループに戻ります。 usbsus2.srfが、usbsus1.srfと異なる点は、割り込みを禁止して5秒間待つループの中で、常にUSB SUSPEND信号の変化を監視していることです。 USB SUSPEND信号が、Lo⇒Hiに変化したら、USBコントローラの割り込みを解除する操作を行います。 割り込み禁止なのでUSB割り込みがかからない代りに、サスペンドしたかどうかをポーリングして、USBコントローラの割り込みを解除しているわけです。 usbsus1.srfとusbsus2.srfの挙動から推測するに、どうやら、推測は正しいようです。 ■何ミリ秒以内に割り込み解除操作を行う必要があるか http://www.piece-me.org/piece-lab/usbsus/usbsus3.c 前述の通り、USBコントローラが完全にサスペンド状態になる前に、CPUがUSBコントローラに対して、割り込み解除操作を行う必要があります。 それでは、USBコントローラがサスペンドへ移行する要因が発生した後、USBコントローラが完全にサスペンド状態になるまで、どれぐらい時間の余裕があるのでしょうか。 usbsus3.srfは、それを検証するアプリです。 usbsus3.srfが、usbsus2.srfと異なる点は、USB SUSPEND信号の変化を検出した後、わざと少し時間を待ってから、割り込み解除操作を行うようにした事です。 待ち時間をいろいろ変えて試したところ、約2ミリ秒以内ならばハングアップせず、約2ミリ秒以上待つとハングアップしました。 どうやら、USBコントローラがサスペンドへ移行する要因が発生した後、USBコントローラが完全にサスペンド状態になるまでの時間は、約2ミリ秒のようです。 セレクティブサスペンドが任意のタイミングで発生することを考慮すると、常に、USB割り込み応答時間が2ミリ秒以内でなければいけないと言うことです。 要するに、割り込み禁止にして行う処理が、どれも、2ミリ秒を超えてはいけないと言うことになります。 P/ECEの一般的なアプリにとって、この条件はかなり厳しいです。(本格的なリアルタイムOSならば話は別でしょうけれど…) 既存の、割り込み禁止にして行っている処理を、全て、2ミリ秒以内に修正するとなると、かなりの変更を要します。 ■インテリジェントDMAを使って解決する そこで、別の方法を考えてみました。 割り込み解除操作として必要な要件は、以下のとおりです。 要件1. USB SUSPEND信号がLo⇒Hiに変化したら、2ミリ秒以内に以下の操作を行うこと。 要件2. USBコントローラに対して、書き込みと読み出しを行うこと。(PDIUSBD12の'Read Interrupt Register'に相当) 要件3. CPUが割り込み禁止状態であっても、処理を行うこと。 ぴったりの機能があります。インテリジェントDMA(IDMA)です。 要件1. ⇒ USB SUSPEND信号(K50端子)をトリガとして、IDMA Ch.1を起動するように設定できます。 要件2. ⇒ IDMAのリンク機能を使うと、書き込みのDMA処理に続いて、読み出しのDMA処理を行うように設定できます。 要件3. ⇒ IDMAは、CPUの割り込み禁止状態とは関係なく起動するので、CPUが割り込み禁止でも処理を行えます。 全ての要件を満たしています。 ■IDMAを使って問題を解決したアプリ&結論 http://www.piece-me.org/piece-lab/usbsus/usbsus4.c usbsus4.srfを実行してください。 操作方法や、動作結果は、usbsus2.srfと同じです。 usbsus4.srfが、usbsus2.srfと異なる点は、USB SUSPEND信号の変化を監視したり割り込み解除操作を行うのが、CPUではなく、IDMAである点です。 usbsus2.srfでは、割り込みを禁止して5秒間待つループの中で、CPUがUSB SUSPEND信号を監視していましたが、usbsus4.srfでは、CPUは何もしません。 CPUの動作としては、一番最初のusbsus1.srfと同じです。 その代りに、アプリの最初で、IDMAが適切に動作するように、IDMAの初期化処理を行っています。 usbsus4.srfを繰り返し操作してみて、問題無く、ハングアップせずに動作することが確認できました。 結論として、セレクティブサスペンドに起因するハングアップの問題は、IDMAを使って解決できることが判りました。 ■補足:サスペンド以外のUSB割り込み発生時にIDMAが悪影響を生じないことの確認 これまで、サスペンド時のUSB割り込みについて考えてきましたが、通常のUSB通信においても、USB割り込みは発生します。 IDMAが、間違って、通常のUSB通信の割り込みをクリアしてしまい、本来CPUが処理すべきUSB割り込みが欠落してしまう恐れは無いでしょうか。 完全には検証できていないのですけれど、これまで試してみた限りは問題が出ていないことと、以下2つの理由から、多分大丈夫だと思います。 理由1。通常のUSB通信と近いタイミングでは、セレクティブサスペンドは発生しません。 PCの設定次第ではありますが、だいたい、通常のUSB通信が途切れてから5秒後ぐらいにセレクティブサスペンドが発生します。 だから、通常のUSB通信の割り込みと、セレクティブサスペンドの割り込みが被ることは、まず無いです。 理由2。万一、両者が被ったとしても、IDMAが行う'Read Interrupt Register'の操作では、通常のUSB通信の割り込みはクリアされません。 通常のUSB通信の割り込みをクリアする操作は、'Read Interrupt Register'ではなく、'Read Endpoint Status'だからです。 IDMAが'Read Interrupt Register'の操作を行っても、通常のUSB通信の割り込み要因はクリアされず、CPUに対する割り込み要求は残るからです。 というわけで、まだ検証が充分ではないのですが、多分大丈夫だと思います。 ■最後に IDMAについては、以前に「P/ECE研究室〜S1C33分室 2002年1月22日 インテリジェントDMAコントローラ」で、使い方を調べたことが有りました。 http://www.piece-me.org/piece-lab/s1c33/20020122.html しかしその後、なかなか、IDMAを有効に利用できる場面が無く、これまでアプリでIDMAを利用したことがありませんでした。 P/ECEカーネルも、IDMAを利用しておらず、P/ECEにおいては、IDMAは全くの無駄機能になってしまっていました。 今回、IDMAの有効な利用方法が見つかって、しかも、IDMAのリンク機能も使うことができて、良かったです。 今回調査した問題と対策の、タイミング図を作成しました。ダウンロードはこちらです: xls形式: http://www.piece-me.org/piece-lab/usbsus/usbsus-timing.xls png形式: http://www.piece-me.org/piece-lab/usbsus/usbsus-timing.png 今回使用した検証アプリ一式の、ダウンロードはこちらです: http://www.piece-me.org/piece-lab/usbsus/usbsus-src.zip * Tue Dec 30 17:15:42 JST 2014 Naoyuki Sawa - USBコントローラ(PDIUSBD12)の省電力設定 P/ECEカーネル1.20は、USBコントローラ(PDIUSBD12)の設定として、D12_NOLAZYCLOCKとD12_CLOCKRUNNINGのどちらのフラグも指定していません。 D12_NOLAZYCLOCKとD12_CLOCKRUNNINGのどちらのフラグも指定していない理由は、以下のように書かれています。 参照資料: http://aquaplus.jp/piece/dl/up118to120.txt >D12_NOLAZYCLOCKとD12_CLOCKRUNNINGは常に不要と思われます。 >PDIUSBD12のマニュアルでは少々説明不足なかんじですが、 > >D12_NOLAZYCLOCK: >USBケーブルはつながっているが、有効な通信が行われていない場合に、外部出力クロック速度を下げる。 >USBコントローラからクロック供給を得て動作するCPU等のための省電力サポート機能。 >このフラグを「指定すると」機能がOFFになり、出力クロックは常にフルスピードとなる。 > >D12_CLOCKRUNNING: >USBケーブルがつながっていないときに、USBC自体のクロックを止める。 >このフラグを「指定すると」機能がOFFになり、USBコントローラは常に動きつづける。 >USBコントローラが停止すると外部出力クロックも停止してしまうので、 >USBコントローラからのクロック供給を得て常に動きつづけたい(USBケーブルがつながっているか >どうかには関係なく)ようなCPUがある場合は、このフラグを指定する。 > >という意味だと思います。 結論から言うと、上記は間違いでした。どこが間違っているかと言うと、以下の二点です。 � �D12_NOLAZYCLOCK」と「D12_CLOCKRUNNING」の効果についての、理解が間違っています。 �⊂陛杜呂里燭瓩寮瀋蠅箸靴董◆�D12_CLOCKRUNNING」を指定しない事は正しいのですが、「D12_NOLAZYCLOCK」を指定しない事が間違っています。 ■� �D12_NOLAZYCLOCK」と「D12_CLOCKRUNNING」の効果 PDIUSBD12のデータシートによると、D12_NOLAZYCLOCKとD12_CLOCKRUNNINGの効果は、以下のように書かれています。 参照資料:「PDIUSBD12 USB interface device with parallel bus Rev. 08 - 20 December 2001 Product data」(PDIUSBD12.pdf) ┃□11.2.3 Set mode ┃┌──┬───────┬────────────────────────────────────────────────────┐ ┃│Bit │Symbol │Description │ ┃├──┼───────┼────────────────────────────────────────────────────┤ ┃│2 │CLOCK RUNNING │ A ‘1’ indicates that the internal clocks and PLL are always running even during Suspend state. │ ┃│ │ │ A ‘0’ indicates that the internal clock, crystal oscillator and PLL are stopped whenever not needed. │ ┃│ │ │ To meet the strict Suspend current requirement, this bit needs to be set to ‘0’. │ ┃│ │ │ The programmed value will not be changed by a bus reset. │ ┃├──┼───────┼────────────────────────────────────────────────────┤ ┃│1 │NO LAZYCLOCK │ A ‘1’ indicates that CLKOUT will not switch to LazyClock. │ ┃│ │ │ A ‘0’ indicates that the CLKOUT switches to LazyClock 1ms after the Suspend pin goes HIGH. │ ┃│ │ │ LazyClock frequency is 30 kHz ± 40%. │ ┃│ │ │ The programmed value will not be changed by a bus reset. │ ┃└──┴───────┴────────────────────────────────────────────────────┘ DLできるURL: http://www.gaw.ru/pdf/Philips/usb/PDIUSBD12.pdf (2014/12/30現在) 説明はこれだけで、他にD12_NOLAZYCLOCKとD12_CLOCKRUNNINGに関する説明やタイミング図も無いようです。 確かに、PDIUSBD12のデータシートは説明不足なかんじです。以下のような疑問が浮かびます。 ・D12_NOLAZYCLOCKが‘1’ならば、サスペンド中も外部出力クロックが低速クロックに切り替わりません。 ・D12_NOLAZYCLOCKが‘0’ならば、サスペンド中に外部出力クロックが低速クロックに切り替わります。 …切り替わらない(‘1’)場合に、高速クロックを出力し続けるのか、外部出力クロックが止まるのかが、はっきりとわかりません。 ・D12_CLOCKRUNNINGが‘1’ならば、サスペンド中も内部クロックが止まりません。 ・D12_CLOCKRUNNINGが‘0’ならば、サスペンド中は内部クロックが止まります。 …という事はわかるるのですが、内部クロックが止まった時に、外部出力クロックも止まるのかどうかが、はっきりとわかりません。 さて、先日ふとしたことから、これまで読んだ事の無かった、PDIUSBD12のFAQのドキュメントを見付けました。 そこには、以下のように書かれていました。 参照資料:「FAQ - PDIUSBD12 1 October 1998」(faq_pdiusbd12.pdf) ┃□3.5 What is the CLKOUT frequency during suspend? ┃The behaviour of the output clock is configured based on the Configuration Byte written using the Set Mode Command (0xF3). ┃┌───────────────┬────────────────────────────────────────┐ ┃│Configuration Byte │CLKOUT │ ┃├───────┬───────┤ │ ┃│No Lazy Clock │Clock Running │ │ ┃├───────┼───────┼────────────────────────────────────────┤ ┃│0 │0 │CLKOUT switches to Lazy Clock on suspend. │ ┃│ │ │The output frequency is 18KHz to 48 KHz. │ ┃│ │ │The PLL clock switched off to reduce current consumption. │ ┃├───────┼───────┼────────────────────────────────────────┤ ┃│1 │0 │The CLKOUT stops on suspend. │ ┃├───────┼───────┼────────────────────────────────────────┤ ┃│0 │1 │CLKOUT switches to Lazy Clock on suspend. │ ┃│ │ │The output frequency is 18KHz to 48 KHz. │ ┃│ │ │The PLL clock remains on. │ ┃├───────┼───────┼────────────────────────────────────────┤ ┃│1 │1 │The suspend state does not affect the CLKOUT frequency with this configuration. │ ┃└───────┴───────┴────────────────────────────────────────┘ DLできるURL: http://ricrdn0.sweb.cz/usb/faq_pdiusbd12.pdf (2014/12/30現在) なるほど、これならばよくわかります。 「D12_NOLAZYCLOCK」と「D12_CLOCKRUNNING」の効果について、正しくは以下の通りでした。 ・D12_NOLAZYCLOCKが‘1’で、D12_CLOCKRUNNINGが‘1’ならば、サスペンド中も外部出力クロックは高速クロックのままです。 ・D12_NOLAZYCLOCKが‘1’で、D12_CLOCKRUNNINGが‘0’ならば、サスペンド中は外部出力クロックが止まります。 ・D12_NOLAZYCLOCKが‘0’ならば、サスペンド中は外部出力クロックが低速クロックになります。 ・D12_CLOCKRUNNINGが‘1’ならば、サスペンド中も内部クロックが止まりません。 ・D12_CLOCKRUNNINGが‘0’ならば、サスペンド中は内部クロックが止まります。 ■�⊂陛杜呂里燭瓩寮瀋� もっとも省電力にするためには、サスペンド中は、内部クロックも外部出力クロックも止めることです。 そのためには、 ・「D12_NOLAZYCLOCK」は指定する。 ・「D12_CLOCKRUNNING」は指定しない。 とするのが正解でした。 現状のP/ECEカーネル1.20は、 ・「D12_NOLAZYCLOCK」は指定しない。 ・「D12_CLOCKRUNNING」も指定しない。 となっています。 P/ECEの回路では、PDIUSBD12の外部出力クロックを使用しないので、常に外部出力クロックが停止していても良いぐらいなのですが、 PDIUSBD12の設定にはそういう設定は無いので、次善の策として、サスペンド中に外部出力クロックを止めるのが望ましいです。 しかし、P/ECEカーネル1.20の設定では、スタンバイ中も外部出力クロックに無駄な低速クロックが出力されてしまっています。 結論としては、D12_NOLAZYCLOCKを指定すべきところ、P/ECEカーネル1.20はD12_NOLAZYCLOCKを指定していないので、無駄な電力を消費していました。 ただ、D12_NOLAZYCLOCKの指定が不足していることによる、無駄な消費電力はさほど大きくないと思います。 「Lazy Clock」と名付けてPDIUSBD12の'売り'の一つにするぐらいなので、低速クロックを外部出力するための消費電力は十分小さいと思うからです。 あと、記憶がおぼろげなのですが、当時、P/ECE掲示板で消費電流の計測をした時に、D12_NOLAZYCLOCKの有無では違いが無かったような気がします。 たぶん、D12_CLOCKRUNNINGの指定の有無による消費電力の違いがほとんどで、D12_NOLAZYCLOCKの有無による違いは小さいのではないでしょうか。 というわけで、現状のP/ECEカーネル1.20のままでも、省電力性能に重大な問題は無いと思います。が、やっぱり気になりますよね(^^; * Mon Dec 29 18:38:36 JST 2014 Naoyuki Sawa - P/ECEカーネル1.20のスタンバイ復帰バグ ■問題点 P/ECEカーネル1.20の変更点の一つとして、以下のように書かれています。 http://aquaplus.jp/piece/dl/up118to120.txt >・スタンバイ状態でUSBケーブルを接続すると、自動的にスタンバイ復帰するようにしました。 しかし実際に試してみると、確かに復帰する事も有るのですが、復帰しない事も結構有るのです。 スタンバイ状態でUSBケーブルを接続してもP/ECEの画面が消えたままで、PC側では「不明なデバイス」と認識してしまいます。 この問題には、だいぶん前から気付いていたのですが、これまで調査していませんでした。 普段、P/ECEに電池を入れずに使っていて、スタンバイ状態にする事も無いので、実用上ほとんど問題にならなかったからです。 今回あらためて、この問題を調査してたところ、原因が判りました。 ■原因 原因は、『スタンバイ復帰用の「ポート入力割り込み2」の割り込みプライオリティが設定されていないこと』でした。 具体的には、P/ECEカーネルのソースの下記の箇所です: \usr\PIECE\sysdev\pcekn\powerman.c 467行 【誤】 bp[0x270] |= 0x04; // ポート入力割り込み2 許可 【正】 bp[0x261] |= 0x07; // ポート入力割り込み2 プライオリティ ←←この処理が抜けていた!! bp[0x270] |= 0x04; // ポート入力割り込み2 許可 スタンバイ状態でUSBケーブルを接続すると、自動的にスタンバイ復帰する仕組みは、以下の通りです。 ポート入力2は、「P22 電源モニタ」端子に相当します。 USBケーブルを接続すると、「P22 電源モニタ」端子が変化して、割り込みが発生します。 割り込みが発生すると、スタンバイ復帰します。 要するに、割り込み発生によってスタンバイ復帰させようとしているのですが、 現在のP/ECEカーネル1.20の処理では、割り込みコントローラの割り込みプライオリティの設定が抜けていました。 割り込みコントローラの割り込みプライオリティレジスタは、イニシャルリセットで初期化されません。 \usr\PIECE\docs\datasheet\EPSON\s1c33209_221_222j.pdf 「S1C33209/221/222 テクニカルマニュアル」B-II-5-12「割り込みコントローラのI/Oメモリ」参照 割り込みプライオリティレジスタの値が、たまたま1〜7ならば、割り込みが発生して、正常にスタンバイ復帰します。 0ならば、割り込みプライオリティ0とは割り込み禁止と同じなので、割り込みが発生せず、スタンバイ復帰しません。 これが、上記の問題の原因でした。 ■対策 根本的な対策は、カーネルを書き換えることなのですが、それは難しいので、運用で回避する対策を考えてみます。 □対策方法�� PCから「ポート入力割り込み2」の割り込みプライオリティを設定します。 具体的には、PCにP/ECEを接続して、DOS窓で以下のようにタイプしてください: isd.exe m 40261 1 これだけでokです。40261というのは、「ポート入力割り込み2」の割り込みプライオリティレジスタのアドレスです。 上記のコマンドで、「ポート入力割り込み2」の割り込みプライオリティを1に設定したことになります。 以下のようにタイプすると、アドレス040261の値が01になっている事を確認できます。(確認は必須ではありません。) isd.exe d 40261 040261 01 04 76 00 01 06 13 77 46 65 01 62 25 01 01 02; ..v....wFe.b%... 〜 P/ECEをスタンバイさせて、USBケーブルを接続すると、正常にスタンバイ復帰します。 この効果は、P/ECEの電池が無くなるまで有効です。スタンバイする度に、コマンドを実行する必要はありません。 □対策方法�� 「ポート入力割り込み2」の割り込みプライオリティを設定する、アプリケーションを実行します。 都合の良いことに、P/ECEに付属しているアプリケーションの中に、そういうアプリケーションがあります。 \usr\PIECE\app\pex\testir.pex 赤外線通信のアプリケーションなのですが、これを実行すると、「ポート入力割り込み2」の割り込みプライオリティが7になります。 赤外線通信の割り込みと、スタンバイ復帰の割り込みは、同じ割り込みプライオリティレジスタを利用しているからです。 赤外線通信アプリケーションをP/ECEに入れておいて、実行してください。すぐに終了して構いません。 P/ECEをスタンバイさせて、USBケーブルを接続すると、正常にスタンバイ復帰します。 この効果は、P/ECEの電池が無くなるまで有効です。スタンバイする度に、アプリケーションを実行する必要はありません。 �,諒�が手軽ですが、P/ECE開発環境がインストールされたPCが必要であるという欠点があります。 �△論岾粟�通信アプリケーションを入れておくためP/ECEの容量を消費しますが、PCが無くてもできる対策です。 ■余談 ところで、'イニシャルリセットで初期化されません。'とは、要するに不定値なのですが、どれぐらいで不定値になるのでしょうか。 試してみたところ、以下のようになりました: ○電池を入れたままでP/ECEのリセットボタンを押しても、値は変化しません。 ○電池を抜いて10分放置して、ふただび電池を入れても、値は変化していませんでした。 ×電池を抜いて一時間放置して、ふただび電池を入れると、値は変化していました。  変化後の値はランダムで、必ずしも0になるとは限らないようです。0になる事が多いみたいですが、1になる事も有りました。 というわけで、前述の対策方法�´△鮃圓辰�P/ECEの電池が切れた場合も、10分以内に電池を交換すれば、対策の効果は持続すると思います。 * Fri Oct 24 21:26:13 JST 2014 Naoyuki Sawa - strtod()のバグ ■問題点 P/ECE開発環境に付属している、EPSON製Cライブラリの、「strtod()関数」には、バグがあります。 『変換に失敗したとき、走査の終了位置を示す文字へのポインタが設定されない』というバグです。 strtod()は、以下のように使うことがよくあると思います。 │ #include │ #include │ static const char str[] = "1.2 -3.4 5.6"; │ int main() { │ const char* ptr = str; │ char* endp; │ double d; │ for(;;) { │ d = strtod(ptr, &endp); │ if(endp == ptr) { break; } //変換に失敗したら、終了する。 │ printf("%g\n", d); │ ptr = endp; //今回の走査の終了位置から、次回の走査を行う。 │ } │ return 0; │ } strtod()関数は、変換に失敗したとき、渡されたptrと同じポインタを、endpに格納することになっています。 上記のプログラムは、ptrとendpが同じならば、変換できる文字列が無くなったと判断して、ループを終了しています。 ところが、P/ECEのstrtod()関数は、「変換に失敗したとき、endpが不定になる」というバグがあるようです。 厳密には、「変換に失敗したとき、endpに値が格納されません」。 strtod()を呼び出す時点でendpは不定値ですので、変換が失敗すると、endpは不定値のままとなります。 プログラムは、strtod()が変換に失敗したことを判断できずに、いつまでもループが終了せずに、暴走します。 さらに悪いことには、不定なendpの位置から次回の走査を行おうとして、無茶苦茶な数値を取得してしまいます。 ■原因 原因は、EPSON製Cライブラリの「strtod()関数」の、バグでした。 変換が失敗したとき、endpが指す変数にポインタを格納すべきところ、間違ったローカル関数にポインタを格納していました。 【誤】 │ if(iChgFlg == 0){ │ if(sStrTmpP != NULL){ │ sStrTmpP = sStrPtrP; │ } │ return (double)0.0; /* no conversion */ │ } ↓ 【正】 │ if(iChgFlg == 0){ │ if(sEndPtrP != NULL){ │ *sEndPtrP = (char*)sStrPtrP; │ } │ return (double)0.0; /* no conversion */ │ } ちなみに、EPSON製Cライブラリの「strtod()関数」には、上記の他にも、(重大性は低いものの)ケアレスミスぽい点があります。 ■対策 二種類の対策方法があります。 □対策� ,箸蠅△┐魂麋鬚垢訛从�方法 「strtod()関数」をあまり頻繁に使わない場合は、その都度対策するのが手っ取り早くて良いと思います。 変換が失敗したときにendpが変わらないのですから、strtod()を呼び出す前にptrと同じポンイタを入れておけば良いのです。 │ endp = ptr; //★EPSON製Cライブラリの「strtod()関数」のバグ対策★ │ d = strtod(ptr, &endp); │ if(endp == ptr) { break; } //変換に失敗したら、終了する。 □対策�◆〆�本的に修正する対策方法 「strtod()関数」を頻繁に使う場合は、その都度上記の対策を行うのは面倒だし、見落とすおそれがあるので、 EPSON製Cライブラリの「strtod()関数」を修正して、置き換える方が安全でしょう。 修正した「strtod.c」を、アプリケーションと一緒にビルドすれば、 「\usr\PIECE\lib\lib.lib」の中にある元の「strtod()関数」よりも優先されて、修正した「strtod()」関数がリンクされます。 修正した「strtod.c」はこちら: http://www.piece-me.org/piece-lab/stodbug/strtod.c ■ダウンロード バグ再現プログラムと、対策プログラムのサンプルは、こちらです。 ダウンロードはこちら: http://www.piece-me.org/piece-lab/stodbug/stodbug-20141024-src.zip * Tue Jul 30 00:00:00 JST 2013 Naoyuki Sawa - gcc33 条件演算子の最適化バグ P/ECE開発環境のCコンパイラgcc33には、条件演算子の最適化バグがあります。 『「0と比較する条件式」の条件演算子をネストすると、間違った結果を返すコードが生成される』というバグです。 以下に、再現方法と、回避方法を説明します。 ■再現プログラム ダウンロードはこちら: http://www.piece-me.org/archive/tropbug-20030730-src.zip //◆コンパイル方法 //┃最適化オプション「-O」「-O1」「-O2」のどれかを指定してコンパイルすると、最適化バグが発生します。 //┃最適化オプション「-O」「-O1」「-O2」どれも指定せずにコンパイルすれば、最適化バグは発生しません。 #include unsigned char vbuff[DISP_X * DISP_Y]; //■関数仕様:aが0でなければaを返す。bが0でなければbを返す。どちらでもなければcを返す。 //□条件演算子を使用する ⇒ ×最適化バグが発生する int test11(int a, int b, int c) { int x; x = a ? a : b ? b : c; return x; } //□条件演算子を使用し、明示的に比較式を書く ⇒ ×最適化バグが発生する int test12(int a, int b, int c) { int x; x = (a != 0) ? a : (b != 0) ? b : c; return x; } //□条件演算子を使用し、明示的に右結合を書く ⇒ ×最適化バグが発生する int test13(int a, int b, int c) { int x; x = a ? a : (b ? b : c); return x; } //□GCC拡張機能を使用する ⇒ ○最適化バグが発生しない int test14(int a, int b, int c) { int x; x = a ? : b ? : c; return x; } //□if文を使用する ⇒ ○最適化バグが発生しない int test15(int a, int b, int c) { int x; if(a) { x = a; } else if(b) { x = b; } else { x = c; } return x; } //■関数仕様:aが0超過ならばaを返す。bが0超過ならばbを返す。どちらでもなければcを返す。 //□条件演算子を使用し、明示的に比較式を書く ⇒ ×最適化バグが発生する int test21(int a, int b, int c) { int x; x = (a > 0) ? a : (b > 0) ? b : c; return x; } //■関数仕様:aが1以上ならばaを返す。bが1以上ならばbを返す。どちらでもなければcを返す。 //□条件演算子を使用し、明示的に比較式を書く ⇒ ×最適化バグが発生する int test31(int a, int b, int c) { int x; x = (a >= 1) ? a : (b >= 1) ? b : c; return x; } //■関数仕様:aが1超過ならばaを返す。bが1超過ならばbを返す。どちらでもなければcを返す。 //□条件演算子を使用し、明示的に比較式を書く ⇒ ○最適化バグが発生しない int test41(int a, int b, int c) { int x; x = (a > 1) ? a : (b > 1) ? b : c; return x; } //■関数仕様:aが0未満ならばaを返す。bが0未満ならばbを返す。どちらでもなければcを返す。 //□条件演算子を使用し、明示的に比較式を書く ⇒ ×最適化バグが発生する int test51(int a, int b, int c) { int x; x = (a < 0) ? a : (b < 0) ? b : c; return x; } //■関数仕様:aが-1以下ならばaを返す。bが-1以下ならばbを返す。どちらでもなければcを返す。 //□条件演算子を使用し、明示的に比較式を書く ⇒ ○最適化バグが発生しない int test61(int a, int b, int c) { int x; x = (a <= -1) ? a : (b <= -1) ? b : c; return x; } //■関数仕様:aが0超過ならばaを返す。bが0未満ならばbを返す。どちらでもなければcを返す。 //□条件演算子を使用し、明示的に比較式を書く ⇒ ×最適化バグが発生する int test71(int a, int b, int c) { int x; x = (a > 0) ? a : (b < 0) ? b : c; return x; } void pceAppInit() { //一般的な初期化 pceLCDDispStop(); pceLCDSetBuffer(vbuff); pceLCDDispStart(); //画面クリア memset(vbuff, 0, sizeof vbuff); pceFontSetPos(0, 0); pceFontSetType(2); //テスト関数を呼び出します pceFontPrintf("test11 %4d\n", test11(0, 123, 0)); pceFontPrintf("test12 %4d\n", test12(0, 123, 0)); pceFontPrintf("test13 %4d\n", test13(0, 123, 0)); pceFontPrintf("test14 %4d\n", test14(0, 123, 0)); pceFontPrintf("test15 %4d\n", test15(0, 123, 0)); pceFontPrintf("test21 %4d\n", test21(0, 123, 0)); pceFontPrintf("test31 %4d\n", test31(0, 123, 0)); pceFontPrintf("test41 %4d\n", test41(0, 123, 0)); pceFontPrintf("test51 %4d\n", test51(0, -123, 0)); pceFontPrintf("test61 %4d\n", test61(0, -123, 0)); pceFontPrintf("test71 %4d\n", test71(0, -123, 0)); //画面転送 pceLCDTrans(); } void pceAppProc(int count) { //SELECTボタンが押されたら終了します if(pcePadGet() & TRG_SELECT) { pceAppReqExit(0); } } void pceAppExit() { //何もしません } ■再現方法 まず、再現プログラムを、最適化無しでコンパイルして実行してみます。 実行結果は、こうなります: http://www.piece-me.org/tropbug_ok.png ┏━━━━━━┓ ┃test11 123┃ ┃test12 123┃ ┃test13 123┃ ┃test14 123┃ ┃test15 123┃ ┃test21 123┃ ┃test31 123┃ ┃test41 123┃ ┃test51 -123┃ ┃test61 -123┃ ┃test71 -123┃ ┗━━━━━━┛ 最適化無しの実行結果が、正しい実行結果です。 次に、再現プログラムを、最適化有りでコンパイルして実行してみます。 実行結果は、こうなります。 http://www.piece-me.org/tropbug_ng.png ┏━━━━━━┓ ┃test11 0┃ ┃test12 0┃ ┃test13 0┃ ┃test14 123┃ ┃test15 123┃ ┃test21 0┃ ┃test31 0┃ ┃test41 123┃ ┃test51 0┃ ┃test61 -123┃ ┃test71 0┃ ┗━━━━━━┛ 最適化有りの実行結果は、一部が違っています。 最適化無しと最適化有りとで、実行結果が同じなのは、test14,test15,test41,test61 です。 最適化無しと最適化有りとで、実行結果が異なるのは、test11,test12,test13,test21,test31,test51,test71 です。 前者が、最適化バグが発生しないケース、後者が、最適化バグが発生するケースとなります。 ■最適化コード 再現プログラムを最適化有りでコンパイルした時に、gcc33が生成するアセンプラコードを、以下に示します。 (見易さのために、整形してコメントを追記してあります。テスト関数以外は関係無いので、省いてあります。) ;//■関数仕様:aが0でなければaを返す。bが0でなければbを返す。どちらでもなければcを返す。 ;//□条件演算子を使用する ⇒ ×最適化バグが発生する test11: ld.w %r10,%r12 ;// x = a; cmp %r10,0 ;// if(a != 0) { goto __L11; } jrne __L11 ;// ld.w %r10,%r13 ;// x = b; ;//バグ。「cmp %r10,0」が足りない ;// jrne __L11 ;// if(a != 0) { goto __L11; } ;//バグ。「if(b != 0)」であるべき ld.w %r10,%r14 ;// x = c; __L11: ;// ret ;// return x; ;//□条件演算子を使用し、明示的に比較式を書く ⇒ ×最適化バグが発生する test12: ld.w %r10,%r12 ;// x = a; cmp %r10,0 ;// if(a != 0) { goto __L12; } jrne __L12 ;// ld.w %r10,%r13 ;// x = b; ;//バグ。「cmp %r10,0」が足りない ;// jrne __L12 ;// if(a != 0) { goto __L12; } ;//バグ。「if(b != 0)」であるべき ld.w %r10,%r14 ;// x = c; __L12: ;// ret ;// return x; ;//□条件演算子を使用し、明示的に右結合を書く ⇒ ×最適化バグが発生する test13: ld.w %r10,%r12 ;// x = a; cmp %r10,0 ;// if(a != 0) { goto __L13; } jrne __L13 ;// ld.w %r10,%r13 ;// x = b; ;//バグ。「cmp %r10,0」が足りない ;// jrne __L13 ;// if(a != 0) { goto __L13; } ;//バグ。「if(b != 0)」であるべき ld.w %r10,%r14 ;// x = c; __L13: ;// ret ;// return x; ;//□GCC拡張機能を使用する ⇒ ○最適化バグが発生しない test14: ld.w %r10,%r12 ;// x = a; cmp %r10,0 ;// if(a != 0) { goto __L14; } jrne __L14 ;// ld.w %r10,%r14 ;// x = c; cmp %r13,0 ;// if(b == 0) { goto __L14; } jreq __L14 ;// ld.w %r10,%r13 ;// x = b; __L14: ;// ret ;// return x; ;//□if文を使用する ⇒ ○最適化バグが発生しない test15: ld.w %r10,%r12 ;// x = a; ld.w %r12,%r14 ;// y = c; ;//間違いではないが、無駄なコードが生成されている cmp %r10,0 ;// if(a != 0) { goto __L15; } jrne __L15 ;// ld.w %r10,%r12 ;// x = c; ;//間違いではないが、無駄なコードが生成されている cmp %r13,0 ;// if(b == 0) { goto __L15; } jreq __L15 ;// ld.w %r10,%r13 ;// x = b; __L15: ;// ret ;// return x; ;//■関数仕様:aが0超過ならばaを返す。bが0超過ならばbを返す。どちらでもなければcを返す。 ;//□条件演算子を使用し、明示的に比較式を書く ⇒ ×最適化バグが発生する test21: ld.w %r10,%r12 ;// x = a; cmp %r10,0 ;// if(a > 0) { goto __L21; } jrgt __L21 ;// ld.w %r10,%r13 ;// x = b; ;//バグ。「cmp %r10,0」が足りない ;// jrgt __L21 ;// if(a > 0) { goto __L21; } ;//バグ。「if(b > 0)」であるべき ld.w %r10,%r14 ;// x = c; __L21: ;// ret ;// return x; ;//■関数仕様:aが1以上ならばaを返す。bが1以上ならばbを返す。どちらでもなければcを返す。 ;//□条件演算子を使用し、明示的に比較式を書く ⇒ ×最適化バグが発生する test31: ld.w %r10,%r12 ;// x = a; cmp %r10,0 ;// if(a > 0) { goto __L31; } jrgt __L31 ;// ld.w %r10,%r13 ;// x = b; ;//バグ。「cmp %r10,0」が足りない ;// jrgt __L31 ;// if(a > 0) { goto __L31; } ;//バグ。「if(b > 0)」であるべき ld.w %r10,%r14 ;// x = c; __L31: ;// ret ;// return x; //■関数仕様:aが1超過ならばaを返す。bが1超過ならばbを返す。どちらでもなければcを返す。 ;//□条件演算子を使用し、明示的に比較式を書く ⇒ ○最適化バグが発生しない test41: ld.w %r10,%r12 ;// x = a; cmp %r10,1 ;// if(a > 1) { goto __L41; } jrgt __L41 ;// ld.w %r10,%r14 ;// x = c; cmp %r13,1 ;// if(b <= 1) { goto __L41; } jrle __L41 ;// ld.w %r10,%r13 ;// x = b; __L41: ;// ret ;// return x; ;//■関数仕様:aが0未満ならばaを返す。bが0未満ならばbを返す。どちらでもなければcを返す。 ;//□条件演算子を使用し、明示的に比較式を書く ⇒ ×最適化バグが発生する test51: ld.w %r10,%r12 ;// x = a; cmp %r10,0 ;// if(a < 0) { goto __L51; } jrlt __L51 ;// ld.w %r10,%r13 ;// x = b; ;//バグ。「cmp %r10,0」が足りない ;// jrlt __L51 ;// if(a < 0) { goto __L51; } ;//バグ。「if(b < 0)」であるべき ld.w %r10,%r14 ;// x = c; __L51: ;// ret ;// return x; ;//■関数仕様:aが-1以下ならばaを返す。bが-1以下ならばbを返す。どちらでもなければcを返す。 ;//□条件演算子を使用し、明示的に比較式を書く ⇒ ○最適化バグが発生しない test61: ld.w %r10,%r12 ;// x = a; cmp %r10,-1 ;// if(a <= -1) { goto __L61; } jrle __L61 ;// ld.w %r10,%r14 ;// x = c; cmp %r13,-1 ;// if(b > -1) { goto __L61; } jrgt __L61 ;// ld.w %r10,%r13 ;// x = b; __L61: ;// ret ;// return x; ;//■関数仕様:aが0超過ならばaを返す。bが0未満ならばbを返す。どちらでもなければcを返す。 ;//□条件演算子を使用し、明示的に比較式を書く ⇒ ×最適化バグが発生する test71: ld.w %r10,%r12 ;// x = a; cmp %r10,0 ;// if(a > 0) { goto __L71; } jrgt __L71 ;// ld.w %r10,%r13 ;// x = b; ;//バグ。「cmp %r10,0」が足りない ;// jrlt __L71 ;// if(a < 0) { goto __L71; } ;//バグ。「if(b < 0)」であるべき ld.w %r10,%r14 ;// x = c; __L71: ;// ret ;// return x; ■結果の考察 □最適化バグが発生するケース � �0と比較する条件式」の条件演算子をネストすると、最適化バグが発生します。⇒test11 �◆�0と比較する条件式」を、明示的に比較式を書いても、最適化バグが発生することに変わりはありません。⇒test12 ��「0と比較する条件式」を、明示的に右結合を書いても、最適化バグが発生することに変わりはありません。⇒test13 �ぁ�0と比較する条件式」とは、「=0」だけでなく、「<0」「≦0」「≧0」「>0」である場合も、最適化バグが発生します。⇒test21,test51  一つ目の条件式と二つ目の条件式が異なっていても、どちらも「0と比較する条件式」であれば、最適化バグが発生します。⇒test71 �ぁ�0と比較する条件式」とは、「結果的に0と比較する条件式」である場合も、最適化バグが発生します。⇒test31  「結果的に0と比較する条件式」とは、たとえば、「≧1」です。  何故こうなるかというと、gcc33は「≧1」の条件式を「>0」に変換してコードを生成するからです。(常に変換するかどうかは不明) □最適化バグが発生しないケース ��GCC拡張機能を使用して、「=0」の条件式を省略した書き方をすると、最適化バグが発生しません。⇒test14 ��条件演算子を使用せず、if文をネストして書くと、最適化バグが発生しません。⇒test15 �А�0と比較する条件式」でない場合は、最適化バグが発生しません。⇒test41 �─峽覯姪�に0と比較する条件式」に変換できる場合であっても、「≦-1」の場合は、最適化バグが発生しません。⇒test61  前述(��)の通りgcc33は「≧1」の条件式を「>0」に変換しますが、何故か「≦-1」を「<0」に変換しないからです。(常に変換しないかどうかは不明) ■まとめ この最適化バグは、かなり危険だと思います。 「変数a,b,cの中で最初にNULLでない値を取得する」という目的で、「a?a:b?b:c;」と書くことが良くあるからです。 たとえば「a=NULL,b=foo,c=NULL;」の場合に、「a?a:b?b:c;」の結果がfooでなくNULLになってしまいます。 「=0」「=NULL」の条件式ならば、GCC拡張機能を使って「a?:b?:c;」と書けば、最適化バグを回避できます。 しかし、大小比較である場合はGCC拡張機能に置き換えられないので、この回避方法は使えません。 □結論:確実に安全を期すためには: ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃「0と比較する条件式」の条件演算子をネストすることは避けて、常にif文のネストを使うようにする ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ 自分でプログラムを書く場合は、上記の点に留意して書けば良いのですが、オープンソース等をP/ECEでコンパイルして使う場合は、もっと要注意です。 特に、前述の「a?a:b?b:c;」が含まれているケースが、多いと思います。 何かうまく動かない場合は、この最適化バグが原因でないか、ソースを目視確認する必要があるでしょう・・・(面倒ですが(^^;) ■今後の課題(TODO) 目視確認するのは面倒だし見落とすおそれがあるので、ある程度チェックを自動化できないか検討します。 たとえば、『C言語ソースを読み込んで、条件演算子のネスト部分を抽出するフィルタ』を作成して、ソース全体から危険そうな部分のみを自動抽出し、 抽出した部分について、『「0と比較する条件式」の条件演算子のネスト』であるかどうかだけを、目視確認すれば済むようにしたいです。 * Thu Jan 19 01:01:11 JST 2012 Naoyuki Sawa - pceFontPrintf()のバグ P/ECE APIのpceFontPrintf()には、バグがあります。 長い文字列を表示しようとすると、ハングアップすることがある、というバグです。 具体的には、「引数を展開中に35文字の境界を超えると、ハングアップする」というものです。 以下に、再現方法と、原因と、回避方法を説明します。 ■再現方法 □引数36文字⇒ハングアップ 以下のコードを実行すると、ハングアップします。 pceFontPrintf("%s", "012345678901234567890123456789012345"); □書式36文字⇒大丈夫 以下のコードを実行しても、ハングアップしません。 pceFontPrintf("012345678901234567890123456789012345"); □書式30文字の末尾に引数6文字を展開⇒ハングアップ 以下のコードを実行すると、ハングアップします。 pceFontPrintf("012345678901234567890123456789%s", "012345"); □書式30文字の先頭に引数6文字を展開⇒大丈夫 以下のコードを実行しても、ハングアップしません。 pceFontPrintf("%s012345678901234567890123456789", "012345"); ■原因 バグの原因は、P/ECEカーネルソース内のマクロ定義が、一箇所誤っているためです。 C:\usr\PIECE\sysdev\pcekn\doprnt.c 88行目 #define PUTC(ch) *pob->outp++ = (ch); if (pob->outp >= pob->endp) pob->flush(pob); PUTC()は、pceFontPrintf()に指定した書式と引数を、一時的なバッファに展開する際、一文字毎にバッファの終端に到達していないかチェックし、 バッファの終端に到達していたら、一旦そこまでを表示して、展開先を再びバッファの先頭に戻すために利用されているマクロです。 以下のコードは、書式の中の文字をそのまま展開しているところです。 これは、問題ありません。 C:\usr\PIECE\sysdev\pcekn\doprnt.c 122行目〜 while ( (ch = *fmt) && ch != '%' ) { PUTC( ch ); fmt++; cnt++; } 以下のコードは、書式に従って、引数を展開しているところです。(他にも何箇所かあります) ここに、問題があります。 C:\usr\PIECE\sysdev\pcekn\doprnt.c 402行目〜 /* the string or number proper */ n = size; while (--n >= 0) PUTC(*t++); 本来、上記のコードは、以下のようにコンパイルされなければなりません。 /* the string or number proper */ n = size; while (--n >= 0) { *pob->outp++ = *t++; if (pob->outp >= pob->endp) pob->flush(pob); } ところが、PUTC()の定義が { } で囲むのを忘れているため、上記のコードは以下のようにコンパイルされてしまいます。 /* the string or number proper */ n = size; while (--n >= 0) { *pob->outp++ = *t++; } if (pob->outp >= pob->endp) pob->flush(pob); 一文字展開する度にバッファの終端に到達しているかをチェックするのでなく、全部展開してからバッファの終端に到達しているかをチェックしてしまいます。 展開の途中でバッファの終端を超えた場合、メモリ破壊が発生してしまうというわけです。 □原因:補足 「引数を展開中に35文字の境界を超えると、ハングアップする」というバグ挙動の、なぜ"35文字"なのかを補足説明します。 C:\usr\PIECE\sysdev\pcekn\doprnt.c 834行目〜 int pceFontPrintf( const char *fmt, ... ) { OUTBF b; char buff[31+1]; int c; b.topp = b.outp = buff; b.endp = buff+sizeof(buff)-1; b.flush = OutFont; c = doprnt( &b, fmt, (va_list)((&fmt)+1) ); OutFont( &b ); return c; } 上記コードの"buff[31+1]"が、展開バッファです。 前述のとおり、引数の展開中に一文字毎にバッファの終端に到達(=31文字を超えた)していないかチェックする処理にバグがあるため、 引数の展開中にバッファの31文字目を超えて32文字目に到達すると、ヌル文字を含めて33文字以上になり、メモリ破壊が発生します。 ところがたまたま、"buff[31+1]"の直後には、変数"c"が4バイト分配置されています。 doprnt()の中で、"buff[31+1]"を突き抜けて変数"c"のメモリを破壊しても、doprnt()が処理を返すまで変数"c"は使用しないので影響がない、というわけです。 35文字を超えると、変数"c"の後ろに格納されているリターンアドレスを破壊してしまい、pceFontPrintf()からのリターン時にハングアップします。 ■回避方法1:簡単な回避方法 簡単な回避方法は、pceFontPrintf()で35文字を超える文字列を表示しないように注意する、という方法です。 厳密には、「引数を展開中に35文字の境界を超えなければ」良いのですが、この条件を事前にチェックするのは難しいです。 最終的な出力が35文字以下になるように注意して使うのが、安全だと思います。 ■回避方法2:確実な回避方法 確実な回避方法は、バグを修正したpceFontPrintf()を、アプリケーションに含めてしまう方法です。 このファイル(http://www.piece-me.org/piece-lab/pceFontPrintf/doprnt.c)を、アプリケーションに含めてコンパイルすればokです。 このファイルは、C:\usr\PIECE\sysdev\pcekn\doprnt.c をコピーして、少しだけ変更したものです。 変更点は、以下の二点です。 ・一点目:88行目 #define PUTC(ch) { *pob->outp++ = (ch); if (pob->outp >= pob->endp) pob->flush(pob); } //{{修正}} 上で説明した、PUTC()のバグを修正しました。この変更は必須です。 ・二点目:787行目〜 //int pcevsprintf( char *outp, const char *fmt0, va_list argp ) //{{削除}} //{ //{{削除}} // OUTBF b; //{{削除}} // b.outp = outp; //{{削除}} // b.endp = (char *)-1; //{{削除}} // return doprnt( &b, fmt0, argp ); //{{削除}} //} //{{削除}} //int pcesprintf( char *outp, const char *fmt, ... ) //{{削除}} //{ //{{削除}} // OUTBF b; //{{削除}} // b.outp = outp; //{{削除}} // b.endp = (char *)-1; //{{削除}} // return doprnt( &b, fmt, (va_list)((&fmt)+1) ); //{{削除}} //} //{{削除}} pcevsprintf()とpcesprintf()を削除しました。この変更は必須ではありませんが、メモリ節約のために削除しました。 pcevsprintf()とpcesprintf()は、P/ECEカーネル内のpcevsprintf()とpcesprintf()を、そのまま使っても大丈夫です。 pcevsprintf()とpcesprintf()は元々、バッファの終端をチェックしていないので、PUTC()のバグがあっても動作結果に違いは無いからです。 回避方法2の欠点は、アプリケーションのコードサイズが増えてしまうことで、3キロバイト近く増えてしまいます。 PUTC()の一行を修正するためだけに、P/ECEカーネル内のpceFontPrintf()を捨てて、アプリケーション内にpceFontPrintf()を抱えてしまうからです。 回避方法2の方が確実で安心ではあるのですが、pceFontPrintf()の使用箇所が少なく充分注意できる場合は、回避方法1の方が良いかも知れません。 □回避方法2:補足 P/ECE APIのバグを修正する場合、厳密には、修正した関数をアプリケーションにリンクするだけでなく、カーネルサービスベクタを登録しなければいけません。 カーネルサービスベクタを登録するには、以下のように行います。 PCEKSENT old_pceFontPrintf; void pceAppInit() { … //カーネルサービスベクタに、修正したpceFontPrintf()を登録する old_pceFontPrintf = pceVectorSetKs(29/*KSNO_FontPrintf*/, fix_pceFontPrintf); … } void pceAppExit() { … //カーネルサービスベクタに、元のpceFontPrintf()を戻す pceVectorSetKs(29/*KSNO_FontPrintf*/, old_pceFontPrintf); … } こうすることによって、アプリケーションだけでなく、カーネル内でAPIを使用している場合も、修正した関数が利用されるようになります─── ───本来はそうであるべきなのですが、残念ながら、pceFontPrintf()については、そのようになりません。 カーネル内からのpceFontPrintf()の呼び出しが、カーネルサービスベクタ経由でなく、直接呼び出しになっているからです。 そんなわけで、カーネル内からのpceFontPrintf()呼び出しは、どうしても、バグ有りのpceFontPrintf()が呼び出されてしまいます。 さいわい、カーネル内でpceFontPrintf()を使っている箇所は、システムメニューやエラー表示など、短い文字列の表示用途ばかりです。 これらの用途で、35文字を超える表示になることは無さそうなので、ひとまず安心です。 ■最後に 今回説明したバグは、P/ECE本体付属の一番古いカーネル(Ver 1.00)から有ったもので、現時点で最新のカーネル(Ver 1.20)でも修正されていませんでした。 pceFontPrintf()に、長い書式文字列を指定することはあっても、長い引数文字列を指定することはあまりないため、顕在化しなかったのかも知れませんね。 今回使用したサンプルプログラム一式は、こちら: http://www.piece-me.org/archive/pceFontPrintf_BugReport-20120119.zip * Wed Apr 06 00:00:00 JST 2005 Naoyuki Sawa - ストリーム再生のまとめ#2 今回もHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/stream/stream2.html * Sat Mar 26 19:35:00 JST 2005 Naoyuki Sawa - ストリーム再生のまとめ#1 今回はHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/stream/stream1.html * Fri Feb 25 06:00:00 JST 2005 Naoyuki Sawa - 続・ディレイド分岐命令の誤動作〜"jp.d %rb"命令は使用不可 2003年12月17日〜23日のP/ECE研究記録「ディレイド分岐命令の誤動作」で、"jp.d %rb"命令の直前にメモリアクセス命令を置くとディレイド命令が実行されない、 という不具合について記しました。 ●ディレイド命令が正しく実行される例 ld.b %r4, %r7 ; メモリアクセス以外の命令 jp.d %r6 ; %r6レジスタの指すアドレスへジャンプ… add %r5, 1 ; …する前に、このディレイド命令を実行します。 ●ディレイド命令が正しく実行される例 ld.b [%r4], %r7 ; メモリアクセス命令 jp.d %r6 ; %r6レジスタの指すアドレスへジャンプ… add %r5, 1 ; …する前に、このディレイド命令を実行しなければいけないのですが、実行されません!! 詳しくは、2003年12月17日〜23日のP/ECE研究記録「ディレイド分岐命令の誤動作」を参照してください。 ----------------------------------------------------------------------------- "jp.d %rb"命令の直前にメモリアクセス命令を置くことさえしなければ正しく動作するのならば、 プログラミング時に気をつけてさえいれば、"jp.d %rb"命令の誤動作を確実に回避することができます。 しかし実際には、誤動作が発生する条件はそれだけではなく、事実上"jp.d %rb"命令は使用不可能であることがわかりました。 その条件とは、 "jp.d %rb"命令の実行と、DMA転送によるメモリアクセスのタイミングが重なると、 "jp.d %rb"命令のディレイスロットに置かれたディレイド命令が実行されない。 というものです。 DMA転送によるメモリアクセスのタイミングを、プログラミング時にサイクル単位で完全に把握することは、まず不可能です。 "jp.d %rb"命令の誤動作を確実に回避することはできず、従って、"jp.d %rb"命令は使用不可、ということになります。 さて、それでは、 "jp.d %rb"命令の実行と、DMA転送によるメモリアクセスのタイミングが重なると、 "jp.d %rb"命令のディレイスロットに置かれたディレイド命令が実行されない。 ことを確認してみましょう。 実験プログラムはこちら: http://www.piece-me.org/archive/jpdbug-20050225.zip 実験プログラムtest1〜test4が、以下の説明の� 銑い紡弍�しています。 ----------------------------------------------------------------------------- �,泙困蓮�"jp.d %rb"命令が正しく動作する例を確認します。 アセンブラで書かれた次のようなテストルーチンをC言語から呼び出して、変数RESULTに格納された値を画面に表示します。 test1: xld.w %r4, 1000000 ; 1000000回ループします。 xld.w %r5, 0 ; 1回毎に1づつカウントアップします。 xld.w %r6, skip ; jp.d命令のジャンプ先アドレスです。 loop: jp.d %r6 ; jp.d命令を使って以下の区間を飛び越えます。 add %r5, 1 ; jp.d命令のディレイスロットでカウントアップを行います。 ; ;{{-------------------------------------------------------- ; この区間は飛び越えるので、実行されません。 ; 本当に飛び越えていることを確認するために、 ; 万一、この区間を飛び越えずに実行されたら、 ; アドレスエラーが発生するようにしておきます。 xld.w %r15, 1 ld.w %r15, [%r15] ;}}-------------------------------------------------------- ; skip: xsub %r4, %r4, 1 ; ループ処理を行います。 xjrne loop xld.w [RESULT], %r5 ; カウントアップの結果を格納します。 ret テストルーチンの内容は、次のとおりです。 1000000回のループを行い、ループが1回まわるたびにカウンタを1づつ増やします。 このとき、「カウンタを1づつ増やす」処理("add %5,1")を、"jp.d %rb"命令のディレイスロットで行っています。 1000000回のループが終了したら、カウンタの値を、グローバル変数RESULTに格納します。 正しく実行されたならば、グローバル変数RESULTには1000000が格納されているはずです。 それでは実行してみます。 結果: 1000000 期待どおり、グローバル変数RESULTには1000000が格納されました。 このテストルーチンでは、"jp.d %rb"命令の直前にメモリアクセス命令が無いので、"jp.d %rb"命令が正しく動作し、 ディレイスロットに置かれたディレイド命令がきちんと実行されていることがわかります。 ----------------------------------------------------------------------------- �⊆,法�"jp.d %rb"命令が誤動作する例を確認します。 2003年12月17日〜23日のP/ECE研究記録「ディレイド分岐命令の誤動作」で確認したのと同じ条件ですけれど、 念のために、今回も、もういちど確認しておくことにしました。 テストルーチンを次のように変更します。 test2: xld.w %r4, 1000000 ; 1000000回ループします。 xld.w %r5, 0 ; 1回毎に1づつカウントアップします。 xld.w %r6, skip ; jp.d命令のジャンプ先アドレスです。 xld.w %r7, DUMMY ; jp.dの誤動作を再現するためのメモリ書き込みアドレスです。 loop: ld.b [%r7], %r7 ; jp.d命令の直前でメモリアクセスすると、jp.dが誤動作します。 ; (高速RAM上で実行する場合は読み書き両方、SRAMの場合は書き込みのみ) jp.d %r6 ; jp.d命令を使って以下の区間を飛び越えます。 add %r5, 1 ; jp.d命令の遅延スロットでカウントアップを行います。 ; ;{{-------------------------------------------------------- ; この区間は飛び越えるので、実行されません。 ; 本当に飛び越えていることを確認するために、 ; 万一、この区間を飛び越えずに実行されたら、 ; アドレスエラーが発生するようにしておきます。 xld.w %r15, 1 ld.w %r15, [%r15] ;}}-------------------------------------------------------- ; skip: xsub %r4, %r4, 1 ; ループ処理を行います。 xjrne loop xld.w [RESULT], %r5 ; カウントアップの結果を格納します。 ret �,箸琉磴い蓮�"jp.d %rb"命令の直前にダミーのメモリアクセス命令を置いたことだけです。 実行してみると、 結果: 84 となりました。(実行するたびに、数字は少し変化します) 本当は1000000となるはずのところ84ですから、ほとんどの"jp.d %rb"命令は誤動作していることになりますが、わずかに正常動作の回があります。 これは何かと言うと、"jp.d %rb"命令の直前で割り込みが発生し、メモリアクセス命令と"jp.d %rb"命令の間で割り込みルーチンが実行された場合に、 メモリアクセス命令と"jp.d %rb"命令が連続で実行されないので、"jp.d %rb"命令が正しく動作するのです。 ためしに、割り込みを完全に禁止して、同じテストルーチンを実行してみると、 結果: 0 となり、"jp.d %rb"命令がすべて異常動作することが確認できます。 なお、この異常動作を確認するテストプログラムは、ときどきハングアップすることがあります。 メモリアクセス命令と"jp.d %rb"命令が連続すると、ディレイド命令が実行されないだけでなく、予測できない動作となることもあるみたいです。 ----------------------------------------------------------------------------- ��さて、ここからが本題です。 まず、テストルーチンの内容を�,汎韻犬發里北瓩靴泙后� "jp.d %rb"命令が正しく動作して、“結果: 1000000”と表示されるはずのテストルーチンです。 ただし、�,箸琉磴い蓮▲丱奪�グラウンドでサウンドを再生しながらテストルーチンを実行する、という点です。 サウンド再生には、通常のP/ECEサウンドAPI pceWaveDataOut() を使います。 実行してみると、 結果: 994836 1%未満ですけれど、"jp.d %rb"命令が誤動作しています。(実行するたびに、数字は少し変化します) �,箸琉磴い蓮▲丱奪�グラウンドでサウンドを再生していることだけですから、サウンド再生が"jp.d %rb"の誤動作の原因になっていることは確実です。 サウンド再生処理の内容は、主に“DMAによるメモリ転送”と“DMA完了割り込み”のふたつです。 このうち、“DMA完了割り込み”が誤動作の原因になっているとは考えづらいです。 なぜなら、サウンド再生を行っていなくても、“DMA完了割り込み”以外の割り込みは頻繁に発生しているからです。 “DMA完了割り込み”以外の割り込みが"jp.d %rb"の誤動作を引き起こさず、“DMA完了割り込み”だけが誤動作を引き起こすとは思えないからです。 そんなわけで、原因は“DMAによるメモリ転送”にあるとアタリを付けてみることにしました。 ----------------------------------------------------------------------------- �ぁ�DMAによるメモリ転送”が"jp.d %rb"命令の誤動作の原因であることを確認しましょう。 サウンド再生処理のような複雑な処理ではなく、純粋にDMA転送だけを行ってみます。 �,筬�と同じテストルーチンのバックグラウンドで単純なDMA転送を行って、"jp.d %rb"命令の誤動作が発生するかどうかを確認します。 テストルーチンを呼び出す前に、次のような手順でDMA転送を開始します。 DISABLE; /*---------- DMAトリガ用タイマの設定 ----------*/ /* クロック選択 = θ/1 (タイマカウントダウン周期 = 24MHz) */ bCLKSEL_T8_P8TPCK3 = 1; /* クロック制御 = On */ bCLKCTL_T8_23_P8TON3 = 1; /* リロードデータ = 240-1 (DMAトリガ周期 = 100KHz) */ pT8_RLD3 = 240-1; /* プリセット、Run。 */ pT8_CTL3 = (1<<1) | (1<<0); /*---------- DMAの設定 ----------*/ /* コントロール情報を設定する前に、高速DMA Ch.3を確実に停止します。 */ HS3_DISABLE; /* トリガ要因 = 8bitタイマCh.3アンダーフロー */ bHSDMA_HTGR2_HSD3S = HS3_8T3; /* アドレスモード = デュアル * 転送カウンタ = 0xffffff回 (このプログラムでは、明示的に停止されるまで) */ pHS3_CNT = (1<<31) | (1<<28)-1; /* 転送データサイズ = バイト * 転送元アドレス制御 = 固定 * 転送元アドレス = &DUMMY1 */ pHS3_SADR = (int)&DUMMY1; /* 転送モード = シングル転送 * 転送先アドレス制御 = 固定 * 転送先アドレス = &DUMMY2 */ pHS3_DADR = (int)&DUMMY2; /* 高速DMA Ch.3を有効にする前に、確実にトリガフラグをクリアします。 */ pHS3_TF = 1; /* 高速DMA Ch.3を有効にします。 */ HS3_ENABLE; ENABLE; DMA転送の内容は、ダミー変数領域から別のダミー変数領域へ、100KHzの頻度でメモリ転送を行う、というものです。 サウンド再生と同様に、DMA転送とテストルーチンは並行して実行されます。 サウンド再生の場合と違って、DMA転送にかかわる割り込みは発生しません。 それでは、実行してみます。 結果: 974426 予想どおり、"jp.d %rb"が誤動作しました。(実行するたびに、数字は少し変化します) サウンド再生の場合よりも、少し誤動作の頻度が多いですね。 その理由は、サウンド再生のDMA転送レートは32KHzであるのに対して、�い離謄好肇廛蹈哀薀爐療樵�レートは100KHzだからです。 DMA転送の発生する頻度が多い分、"jp.d %rb"と重なる確率が高く、結果、"jp.d %rb"が誤動作する頻度も多くなるわけです。 ためしに、DMA転送レートを10倍に増してみると、 /* リロードデータ = 24-1 (DMAトリガ周期 = 1MHz) */ pT8_RLD3 = 24-1; "jp.d %rb"誤動作の頻度がもっと高くなることが確認できます。 結果: 699436 ----------------------------------------------------------------------------- 以上の実験で、 "jp.d %rb"命令の実行と、DMA転送によるメモリアクセスのタイミングが重なると、 "jp.d %rb"命令のディレイスロットに置かれたディレイド命令が実行されない。 ことが確認できました。 先にも述べたように、DMA転送のタイミングに依存する誤動作は、回避不可能です。 P/ECEの場合、サウンド再生と併用しなければ良いのですけれど、それは現実的ではありません。 結論は、 ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃"jp.d %rb"命令は、いかなる場合においても★絶対に★使ってはいけない┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ となります。 なお、P/ECE開発環境の標準Cコンパイラは最適化機能が弱く、"jp.d %rb"命令を生成しませんので、C言語だけを使ってプログラミングしている限りは安全です。 アセンブラを使って、手作業で最適化する場合には、要注意です。 ----------------------------------------------------------------------------- さて、以下は余談です。 "jp.d %rb"命令とDMA転送の併用による誤動作に気付いた経緯について、記しておこうと思います。 現在、「ゲームボーイ/EMU」の作成中なのですけれど、サウンドを追加したとたんに、CPUエミュレーションが正しく実行されなくなるという問題が発生しました。 最初、サウンドエミュレーションルーチンにバグがあって、不正なメモリを破壊しているのかと考えて調べてみたのですが、いくら調べても間違っていません。 原因を突き詰めていくと、サウンドエミュレーションルーチンを呼ばなくても、単にpceWaveDataOut()を呼ぶだけで、問題が発生することがわかりました。 そこで、pceWaveDataOut()を呼んだ場合と呼ばない場合とで、CPUエミュレーションのトレースログを比較してみました。 すると、pceWaveDataOut()を呼んだ場合に、ときどき、残りCPU実行サイクルのカウントダウンが行われていないことがわかりました。 こんなかんじです。 pceWaveDataOut()を呼ばない場合 pceWaveDataOut()を呼んだ場合 ------------------------------ ------------------------------ PC=1000: 残り実行サイクル=440 PC=1000: 残り実行サイクル=440 PC=1001: 残り実行サイクル=436 PC=1001: 残り実行サイクル=436 PC=1002: 残り実行サイクル=432 PC=1002: 残り実行サイクル=436 <- 減ってない!! PC=1003: 残り実行サイクル=428 PC=1003: 残り実行サイクル=432 PC=1004: 残り実行サイクル=424 PC=1004: 残り実行サイクル=428 PC=1005: 残り実行サイクル=420 PC=1005: 残り実行サイクル=424 PC=1006: 残り実行サイクル=416 PC=1006: 残り実行サイクル=420 上の例では、1002番地の命令のエミュレーションで誤動作が発生していますが、実行するたびに誤動作が発生するアドレスが変化するのです。 何度か試してみて、誤動作が発生する命令のエミュレーションルーチンの共通性に気が付きました。 それらのエミュレーションルーチンはすべて、次のようなコード並びを使っていたからです。 ld.w %r0, %r4 ; (メモリアクセス以外の命令。"jp.d %r11"は誤動作しないはず、だったのですけれど…) jp.d %r11 ; メモリ書き込みルーチンへジャンプ sub %r7, 4 ; "jp.d %rb"命令のディレイストットで、残り実行サイクルを減らす つまり、ときどき"sub %r7,4"が実行されていないことになります。 以前のP/ECE研究記録「ディレイド分岐命令の誤動作」で、"jp.d %rb"とメモリアクセス命令の相性が悪いことは既にわかっていました。 サウンドを追加した途端に発生した誤動作、メモリアクセス、、、となると、サウンド再生のためのDMA転送しか考えられません。 そこで今回のような調査を行ってみたところ、"jp.d %rb"命令とDMA転送の併用で異常動作が発生することがわかった、というわけです。 今回は、単純で定型的な処理を繰り返すCPUエミュレーションルーチンの中での誤動作だったために、比較的簡単に原因が推測できました。 もしも、通常のゲームプログラムの入り組んだルーチンの中で、ごく稀に発生するような誤動作だったならば、原因究明にはもっと時間がかかっていたと思います。 ある意味、幸運だったかも知れません。 * Fri Feb 11 04:56:00 JST 2005 Naoyuki Sawa - 続・実はディレイド命令 今回のテーマは、2003年4月12日の記録「実はディレイド命令」の続きです。 ====================================================================================================================== まずは、前回のおさらいから。 P/ECEのCPU S1C33209は、RISC CPUの特徴のひとつ、「ディレイド分岐機能」を持っています。 ディレイド分岐機能とは、分岐命令の実行より先に分岐命令の直後の命令を実行し、実行効率を稼ぐ機能です。 ディレイド分岐機能を使う分岐命令を、「ディレイド分岐命令」と呼びます。 また、ディレイド分岐命令の直後に置かれる命令を、「ディレイド命令」と呼びます。 ディレイド分岐命令直後の、ディレイド命令を置くことができる位置を、「ディレイドスロット」と呼びます。 すべての命令が、ディレイド命令として使用できるわけではありません。 ある命令がディレイド命令として使用できるか否かについて、基本的な判断基準は次の通りです。 ディレイド命令は以下の条件をすべて満たしていることが必要です。 ・1サイクル命令 ・メモリをアクセスしない ・ext命令による拡張なし 『S1C33209コアCPUマニュアル』(C:\usr\PIECE\docs\datasheet\EPSON\33000Core-J.pdf) p.30「ディレイド分岐機能」より 同ページには、ディレイド命令として使用できる命令の一覧が示されています。 さらにその下に、次のような注意が記されています。 注:上記の条件を満たさない命令は動作が不定となるため、ディレイド命令として使用することは禁止します。 ところが、EPSON社自身による標準ライブラリの実装が、このルールを破っていました。 ディレイド命令として使用できないはずの"ld.w %rd,%ss"命令を、ディレイド命令として使用していたのです。 何通りかの命令並びを試してみたところ、"ld.w %rd,%ss"命令は、ディレイド命令として問題なく使用できることがわかりました。 EPSON社も使っているのですから、たぶん大丈夫なのでしょう。 以上、前回のおさらいでした。 ====================================================================================================================== その後、自作プログラムで"ld.w %rd,%ss"命令をディレイド命令として使ってきましたが、これまでのところ問題は出ていません。 やっぱり、大丈夫だったようです。 さて、ディレイド命令として使用不可とされている命令のなかに、"ld.w %rd,%ss"命令よりもっと使用頻度の高いものがあります。 char⇔int変換やshort⇔int変換を行う、以下の命令群です。 ld.b %rd,%rs char⇔int変換 ld.ub %rd,%rs unsigned char⇔int変換、(value&0xff)の代用 ld.h %rd,%rs short⇔int変換 ld.uh %rd,%rs unsigned short⇔int変換、(value&0xffff)の代用 以降の文では、これらの命令を“型変換命令”と呼ぶことにします。 型変換命令はすべて「1サイクル命令」で「メモリをアクセスしない」命令で「ext命令による拡張なし」の条件を満たしています。 にもかかわらず、『S1C33209コアCPUマニュアル』では、ディレイド命令として使用可能な命令に挙げられていません。 実際にプログラムを組んでいると、型変換命令がディレイド命令として使えないために、悔しい思いをすることがよくあります。 add %r12, %r13     ; a = a + b ld.ub %r10, %r12    ; return (unsigned char)a ret ↓"ld.ub %r10,%r12"がディレイド命令として使用可能だったならば、次のように並び替えて高速化できるのですが・・・ add %r12, %r13     ; a = a + b ret.d          ; return (unsigned char)a ld.ub %r10, %r12    ; *delay* 実際には、"ld.ub %r10,%r12"がディレイド命令として使用可能でないために、並び替え“不可”です!! 一見、ディレイド命令として使用可能な"and"命令で代用してしまえばいいようにも見えますけれど、それはできません。 add %r12, %r13     ; a = a + b ret.d          ; return a & 0xff xand %r10, %r12, 0xff  ; *delay* ↓"xand %r10,%r12,0xff"は、コンパイラによって次のように展開されます。 add %r12, %r13     ; a = a + b ret.d          ; return a & 0xff ext 0xff        ; ┐ and %r10, %r12     ; ┴ 2命令に展開されてしまい、ディレイドスロットに収まりません!! 型変換命令を使うときは、ディレイド分岐機能を使った高速化をあきらめるしかないのでしょうか? ====================================================================================================================== 「1サイクル命令」「メモリをアクセスしない」「ext命令による拡張なし」の条件を満たしているにもかかわらず、 ディレイド命令として使用可能でない命令としては、型変換命令の他に、"div1"や"halt"命令などがあります。 "div1"や"halt"命令はやや特殊な部類の命令ですから、ディレイド命令として使用可能でなくても、なんとなく納得できます。 しかし、型変換命令のような単純で使用頻度の高い命令がディレイド命令として使用可能でないというのは、納得できません。 そこでふと考えたのが、「もしかして実際には型変換命令もディレイド命令として使用可能なのではないか?」 思えば"ld.w %rd,%ss"命令だって、マニュアルには記載されていないけれど、暗黙公然のディレイド命令だったわけですから。 それではさっそく試してみましょう。 --------------------------------------------------------- foo: add %r12, %r13     ; a = a + b ret.d          ; return (unsigned char)a ld.ub %r10, %r12    ; *delay* --------------------------------------------------------- このアセンブラルーチンを、C言語プログラムから呼び出してみます。 --------------------------------------------------------- extern int foo(int a, int b); n = foo(0x12345678, 0x9abcdef0); pceFontPrintf("%08x", n); --------------------------------------------------------- 結果は、「00000068」と表示されました。 0x12345678 + 0x9abcdef0 = 0xacf13568 (unsigned char)0xacf13568 = 0x68 ですから、正しい結果です。 実行時間を計測してみると、ディレイド分岐機能を使ったことによる高速化の効果が確認できました。 前後数命令の並びを何通りかに変えてみたり、プログラムやデータをSRAMや高速RAMに配置しても、すべて正しい結果となります。 "ld.b %rd,%rs"、"ld.h %rd,%rs"、"ld.uh %rd,%rs"命令についても同様に試してみたところ、すべて正しい結果が得られました。 三年間悩んでいたのが可笑しくなってくるぐらい、あっけない幕切れです。 ┏━━━━━━━━━━━━━━━━━━━━┓ ┃┏━━━━━━━━━━━━━━━━━━┓┃ ┃┃ 型変換命令もディレイド命令でした ┃┃ ┃┗━━━━━━━━━━━━━━━━━━┛┃ ┗━━━━━━━━━━━━━━━━━━━━┛ もっとも、テストプログラムで正しい結果が得られただけでは、あらゆる場面で正しい結果が得られるとは限りません。 マニュアルにも「上記の条件を満たさない命令は動作が★不定★となるため〜」と記されているように、 テストプログラムではたまたま正しい結果が得られただけかも知れないからです。 しかし、"ld.w %rd,%ss"の前例からすると、たぶんどんな場面でも大丈夫な気がします。僕のカンですけれど。。。(^^; 型変換命令がディレイド命令として使えることのメリットは、もしかしたら不安定かも…というリスクを上回ります。 なにより、使ってみなければ不安定かどうかもわかりませんから、今後は積極的に使ってみることにしようと思います。 ====================================================================================================================== そんなわけで、型変換命令もディレイド命令として(たぶん)使用可能であることがわかりました。 もっと早くに試しておけばよかった。。。 さて、こうなってくると、「型変換命令以外にも、実はディレイド命令として使用可能な命令があるのでは?」と思えてきます。 ちょっと試してみたところ、 ・1サイクル命令 ・メモリをアクセスしない ・ext命令による拡張なし の条件をまったく無視して、ほとんどの命令がディレイド命令として(一応は)動作することがわかりました。 xjp.d bar mlt.w %r12, %r13    ; 5サイクル命令!! とか、 xjp.d bar ld.w %r10, [%r12]    ; メモリをアクセスする!! とか、 xjp.d bar ext 0xff        ; 分岐先の命令のext拡張を先取り!! . . . bar: and %r10, %r12     ; "xand %r10, %r12, 0xff"になります とか、軽く試した限りでは、すべて正しい結果が得られました。 実行時間を計測してみると、ディレイド分岐機能を使ったことによる高速化の効果が確認できます。 特に二つ目の、メモリアクセス命令をディレイドスロットに入れた例で、大幅な高速化の効果があるみたいです。 しかしながらさすがにこれらの複雑な命令になってくると、状況によっては結果が不安定になるのではないかという気もします。 とりあえず今回は「型変換命令もディレイド命令」という結果に満足し、その他の命令の実験はまた今後、としたいと思います。 * Sun Dec 5 16:00:00 JST 2004 Naoyuki Sawa - ADPCMの仕組み#5 今回も絵が多いのでHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/adpcm/adpcm5.html * Sat Dec 4 14:45:00 JST 2004 Naoyuki Sawa - ADPCMの仕組み#4 今回も絵が多いのでHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/adpcm/adpcm4.html * Fri Dec 3 00:00:00 JST 2004 Naoyuki Sawa - ADPCMの仕組み#3 今回も絵が多いのでHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/adpcm/adpcm3.html * Wed Dec 1 00:00:00 JST 2004 Naoyuki Sawa - ADPCMの仕組み#2 今回も絵が多いのでHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/adpcm/adpcm2.html * Tue Nov 30 06:00:00 JST 2004 Naoyuki Sawa - ADPCMの仕組み#1 今回は絵が多いのでHTMLで書きました。以下のURLをご覧下さい。 http://www.piece-me.org/piece-lab/adpcm/adpcm1.html * Sat Nov 7 09:00:00 JST 2004 Naoyuki Sawa - 構造体パックに問題あり#2 前回、P/ECE開発環境では、構造体パックのための__attribute__((packed))が正しく動作しない、というところまでお話しました。 具体的には、構造体サイズは正しくパックされるのだけれど、初期化済み構造体の内容がパックされていない、という症状です。 今回は、その原因を探ってみます。 結論だけ言うと一言で終わってしまうのですが、それでは面白くないので(^^;、僕が調査したときの手順を追って説明してみます。 長いですけれど、おつきあいください。 ■■■■■■■■■■■■■■■■ ■コンパイラ出力を比較してみる■ ■■■■■■■■■■■■■■■■ さてそれでは、構造体アライメントあり(=構造体パック無し)の場合と構造体アライメント無し(=構造体パックあり)の場合とで、 C言語ソースがどのようにコンパイルされ、どのようなアセンブラコードが生成されているのか見てみます。 C言語ソースの内容は、前回の後半■P/ECEの場合〜問題発生!!■を参照してください。 まずは、後半部分にある、pceAppProc()関数がどのようにコンパイルされているのか見てみます。 最初に、__attribute__((packed))を指定しない、構造体アライメントありの場合。 [構造体アライメントあり] ; void pceAppProc(int count) ; { ;   int i; ;   unsigned char* p; ; ;   memset(vbuff, 0, sizeof vbuff); ;   pceFontSetType(2); ;   pceFontSetPos(0, 0); ; ;   p = (unsigned char*)&cid; ;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO)); ;   for(i = 0; i < sizeof(MMC_CID_INFO); i++) { ;     pceFontPrintf("%02x ", p[i]); ;   } ; ;   pceLCDTrans(); ; } ; ; の部分が、次のようにコンパイルされます。 pceAppProc:     pushn %r2     xsub %sp,%sp,8     xld.w %r12,vbuff     ld.w %r13,0x0     xld.w %r14,0x00002c00     xcall memset     xld.w %r12,0x00000002     xcall pceFontSetType     ld.w %r12,0x0     ld.w %r13,%r12     xcall pceFontSetPos     xld.w %r10,__LC0     xld.w [%sp],%r10     xld.w %r10,0x00000014    ; 0x00000014(20) = sizeof(MMC_CID_INFO)     xld.w [%sp+4],%r10     xcall pceFontPrintf     xld.w %r2,__LC1     xld.w %r0,cid     xadd %r1,%r0,15 __L6:     xld.w [%sp],%r2     xld.ub %r10,[%r0]     xld.w [%sp+4],%r10     xcall pceFontPrintf     xadd %r0,%r0,1     cmp %r0,%r1     xjrule __L6     xcall pceLCDTrans     xadd %sp,%sp,8     popn %r2     ret 次に、__attribute__((packed))を指定した、構造体アライメント無しの場合。 [構造体アライメント無し] ; void pceAppProc(int count) ; { ;   int i; ;   unsigned char* p; ; ;   memset(vbuff, 0, sizeof vbuff); ;   pceFontSetType(2); ;   pceFontSetPos(0, 0); ; ;   p = (unsigned char*)&cid; ;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO)); ;   for(i = 0; i < sizeof(MMC_CID_INFO); i++) { ;     pceFontPrintf("%02x ", p[i]); ;   } ; ;   pceLCDTrans(); ; } ; ; の部分が、次のようにコンパイルされます。 pceAppProc:     pushn %r2     xsub %sp,%sp,8     xld.w %r12,vbuff     ld.w %r13,0x0     xld.w %r14,0x00002c00     xcall memset     xld.w %r12,0x00000002     xcall pceFontSetType     ld.w %r12,0x0     ld.w %r13,%r12     xcall pceFontSetPos     xld.w %r10,__LC0     xld.w [%sp],%r10     xld.w %r10,0x00000010    ; 0x00000010(16) = sizeof(MMC_CID_INFO)     xld.w [%sp+4],%r10     xcall pceFontPrintf     xld.w %r2,__LC1     xld.w %r0,cid     xadd %r1,%r0,15 __L6:     xld.w [%sp],%r2     xld.ub %r10,[%r0]     xld.w [%sp+4],%r10     xcall pceFontPrintf     xadd %r0,%r0,1     cmp %r0,%r1     xjrule __L6     xcall pceLCDTrans     xadd %sp,%sp,8     popn %r2     ret sizeof(MMC_CID_INFO)の値が、構造体アライメントありの場合はパディング込みで20バイト、構造体アライメント無しの場合は16バイトになっています。 P/ECE開発環境のCコンパイラは、構造体アライメントありの場合と無しの場合とで、構造体サイズの違いをきちんと処理できていることがわかります。 構造体サイズが変化すること以外は、構造体アライメントありの場合も無しの場合も、まったく同じコンパイル結果です。 問題ありません。 それでは、前半部分の、初期化済み構造体の定義がどのようにコンパイルされているか見てみます。 最初に、__attribute__((packed))を指定しない、構造体アライメントありの場合。 [構造体アライメントあり] ; //CID(Card IDentification number) カードIDレジスタ情報構造体 ; typedef struct tag_MMC_CID_INFO ; { ;   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit) ;   unsigned short usOID;       // + 2, 2: OEM/Application ID(16bit) ;   unsigned char  ucPNM[6];      // + 4, 6: Product name(48bit) ;   unsigned char  ucPRV;       // +10, 1: Product revision(8bit) ;   unsigned long  ulPSN;       // +12, 4: Product serial number(32bit) ;   unsigned char  ucMDT;       // +16, 1: Manufacturing date(8bit) ;   unsigned char  ucCRC;       // +17, 1: 7-bit CRC checksum(7bit) ; } __attribute__((packed)) MMC_CID_INFO; // =20 ; ; MMC_CID_INFO cid = { ;   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit) ;   0xffff,                 // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit) ;   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit) ;   0xff,                  // unsigned char  ucPRV;  // +10, 1: Product revision(8bit) ;   0xffffffff,               // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit) ;   0xff,                  // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit) ;   0xff,                  // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit) ; }; ; ; の部分が、次のようにコンパイルされます。     .align 2 cid:     .byte  255     .space 1     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .space 1     .word  -1     .byte  255     .byte  255     .space 2 次に、__attribute__((packed))を指定した、構造体アライメント無しの場合。 [構造体アライメント無し] ; //CID(Card IDentification number) カードIDレジスタ情報構造体 ; typedef struct tag_MMC_CID_INFO ; { ;   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit) ;   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit) ;   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit) ;   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit) ;   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit) ;   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit) ;   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) ; } __attribute__((packed)) MMC_CID_INFO; // =16 ; ; MMC_CID_INFO cid = { ;   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit) ;   0xffff,                 // unsigned short usOID;  // + 1, 2: OEM/Application ID(16bit) ;   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 3, 6: Product name(48bit) ;   0xff,                  // unsigned char  ucPRV;  // + 9, 1: Product revision(8bit) ;   0xffffffff,               // unsigned long  ulPSN;  // +10, 4: Product serial number(32bit) ;   0xff,                  // unsigned char  ucMDT;  // +14, 1: Manufacturing date(8bit) ;   0xff,                  // unsigned char  ucCRC;  // +15, 1: 7-bit CRC checksum(7bit) ; }; ; ; の部分が、次のようにコンパイルされます。     .align 2 cid:     .byte  255     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .word  -1     .byte  255     .byte  255 おや?一見問題ないように見えます。 わかりやすいように、コメントに付けてみましょう。 [構造体アライメントあり]     .align 2    ; 次の位置が確実に(1<<2)=4の倍数アドレスから始まるようにする。 cid:     .byte  255   ; // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)     .space 1    ; // (パディング)       // + 1, 1:     .half  65535  ; // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit)     .byte  255   ; // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit)     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ; // unsigned char  ucPRV;  // +10, 1: Product revision(8bit)     .space 1    ; // (パディング)       // +11, 1:     .word  -1    ; // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit)     .byte  255   ; // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit)     .byte  255   ; // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit)     .space 2    ; // (パディング)       // +18, 2: 構造体アライメントありの場合については、完全に問題ありません。 前回行ったP/ECE上でのテストでも、期待通りの実行結果となっていました。 構造体アライメントありの場合については、もう検討する必要はなさそうです。 問題は、構造体アライメント無しの場合です。 [構造体アライメント無し]     .align 2    ; 次の位置が確実に(1<<2)=4の倍数アドレスから始まるようにする。 cid:     .byte  255   ; // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)     .half  65535  ; // unsigned short usOID;  // + 1, 2: OEM/Application ID(16bit)     .byte  255   ; // unsigned char  ucPNM[6]; // + 3, 6: Product name(48bit)     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ; // unsigned char  ucPRV;  // + 9, 1: Product revision(8bit)     .word  -1    ; // unsigned long  ulPSN;  // +10, 4: Product serial number(32bit)     .byte  255   ; // unsigned char  ucMDT;  // +14, 1: Manufacturing date(8bit)     .byte  255   ; // unsigned char  ucCRC;  // +15, 1: 7-bit CRC checksum(7bit) コンパイル結果のアセンブラコードを見る限りは、問題無さそうに見えます。 が、実は、上のコメントのようなメモリ配置にはならないのです。 『アセンブラにも暗黙のアライメント機能がある』というのがその理由です。 ■■■■■■■■■■■■■■■■ ■アセンブラのアライメント機能■ ■■■■■■■■■■■■■■■■ P/ECE開発環境に入っている、「S1C33 Family Cコンパイラパッケージ Ver.4」のマニュアルを開いてみてください。 (C:\usr\PIECE\docs\datasheet\EPSON\S5U1C33000C_J.pdf) マニュアル171ページ(PDFでは187ページ目)、「11.8.6 データ定義擬似命令」のところに、次のような記述があります。 .word擬似命令 ●注意事項 ・定義したデータは直前に.align命令がない限り、ワード境界アドレスから配置されます。  現在位置がワード境界アドレス以外の場合、データを配置するワード境界アドレスまでの間は0x00が設定されます。 .half擬似命令 ●注意事項 ・定義したデータは直前に.align命令がない限り、ハーフワード境界アドレスから配置されます。  現在位置が奇数アドレスの場合、現在位置には0x00が設定されます。 これです!! つまり、コンパイル結果のアセンブラコードがas33.exeによってアセンブルされると、次のようなメモリ配置になっていたのです。 [構造体アライメント無し]     .align 2    ; 次の位置が確実に(1<<2)=4の倍数アドレスから始まるようにする。 cid:     .byte  255   ; // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)     (0x00)      ; // (パディング)       // + 1, 1:     .half  65535  ; // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit)     .byte  255   ; // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit)     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ;     .byte  255   ; // unsigned char  ucPRV;  // +10, 1: Product revision(8bit)     (0x00)      ; // (パディング)       // +11, 1:     .word  -1    ; // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit)     .byte  255   ; // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit)     .byte  255   ; // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit) 構造体末尾のパディングが考慮されないことを除けば、このメモリ配置はCコンパイラによって構造体アライメントが行われた場合と同じです。 __attribute__((packed))が指定された時、Cコンパイラは明示的なパディング(.space擬似命令)のコード出力を省いて、構造体アライメントを無くしたつもりでいるのですが、 実際にはアセンブラによってフィールド単位のアライメントが行われてしまい、結果、構造体アライメントありの場合と同じメモリ配置になってしまっていた、というわけです。 では、どのようなアセンブラコード出力ならば、正しく構造体アライメント無しのメモリ配置になるのでしょうか? .align擬似命令が、Visual C++における#pragma packみたいなものだと考えると、次のようなアセンブラコードを思いつきます。 [構造体アライメント無しにするには?]     .align 0    ; 次の位置が(1<<0)=1の倍数アドレスから始まるようにする。 cid:     .byte  255     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .word  -1     .byte  255     .byte  255 が、これではダメです。 実際にプログラムを書いてみて、これではダメなことを確認しました。 [構造体アライメント無しにするには?(失敗例)] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 //初期化済み構造体の定義を、インラインアセンブラで書き直してみます。 extern MMC_CID_INFO cid; asm("     .align 0    ; 次の位置が(1<<0)=1の倍数アドレスから始まるようにする。 cid:     .byte  255     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .word  -1     .byte  255     .byte  255 "); void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ------------------------------------------------------------------------------------------- C言語で書いた場合の、誤った出力結果と変わっていません。 なぜダメか、マニュアル174ページ(PDFでは190ページ目)の、.align擬似命令の説明を見てください。 .align擬似命令 ●機能  この擬似命令の直後に出現するデータを2^Nバイト境界にアライメントします(N=境界指定値)。 ●注意事項  .align擬似命令は直後にあるデータ定義擬似命令に対してのみ有効です。  したがって、アライメントが必要なデータを定義する場合には、データ定義擬似命令個々に.align擬似命令を使用する必要があります。 そう、.align擬似命令は『直後の』データ定義擬似命令に対してしか、効果がないのです。 直後よりうしろのデータ定義擬似命令は、通常どおり、暗黙のアライメントがなされてしまいます。     .align 0    ; 次の位置が(1<<0)=1の倍数アドレスから始まるようにする。 cid:     .byte  255     .half  65535  ; ここから後ろは、通常どおり、暗黙のアライメントがなされてしまいます。     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .word  -1     .byte  255     .byte  255 直後のデータ擬似命令に対して.alignが有効だとういうのならば、次のように書けばいいのでしょうか? [構造体アライメント無しにするには?]     .align 2    ; 構造体の先頭を4の倍数アドレスから始めるために、先頭の.align 2は必要!! cid:     .byte  255     .align 0    ; 次の.halfの暗黙のアライメントを抑制する。     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .align 0    ; 次の.wordの暗黙のアライメントを抑制する。     .word  -1     .byte  255     .byte  255 それでは、もういちどプログラムを書いて試してみます。 [構造体アライメント無しにするには?(失敗例)] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 //初期化済み構造体の定義を、インラインアセンブラで書き直してみます。 extern MMC_CID_INFO cid; asm("     .align 2    ; 構造体の先頭を4の倍数アドレスから始めるために、先頭の.align 2は必要!! cid:     .byte  255     .align 0    ; 次の.halfの暗黙のアライメントを抑制する。     .half  65535     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .byte  255     .align 0    ; 次の.wordの暗黙のアライメントを抑制する。     .word  -1     .byte  255     .byte  255 "); void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ------------------------------------------------------------------------------------------- やっぱりダメです。 何通りか試した結果、どうやら.align擬似命令は「パディングを増やすことはできるが減らすことはできない」ことがわかりました。 たとえば、通常、.byte擬似命令には暗黙のアライメントはありません。 が、構造体の先頭フィールドがunsigned char型であるような場合など、unsigned char値を強制的に4の倍数アドレスに置きたい場合に、 .byte擬似命令の前に.align 2を指定したりするわけで、このような.align擬似命令の使い方はOKです。 が、逆に、もともと暗黙のアライメントによって.half擬似命令や.word擬似命令の直前にパディングが追加されていたものとして、 そのパディングを取り除くために.align 0を指定しても、それは無視されてしまうのです。 以上の振る舞いはマニュアルには載っていないようですけれど、まあ、感覚的に理解できなくもありません。 そんなわけで結局、アセンブラによる暗黙のアライメントを抑制するには、次のように1バイトづつ定義するしかないみたいです。 [構造体アライメント無しにするには?(いちおう成功例ですが…)] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 //初期化済み構造体の定義を、インラインアセンブラで書き直してみます。 extern MMC_CID_INFO cid; asm("     .align 2        ; 構造体の先頭を4の倍数アドレスから始めるために、先頭の.align 2は必要!! cid:     .byte  255       ; // + 0, 1: ManufactureID(8bit)     .byte 255,255     ; // + 1, 2: OEM/Application ID(16bit)     .byte  255       ; // + 3, 6: Product name(48bit)     .byte  255       ;     .byte  255       ;     .byte  255       ;     .byte  255       ;     .byte  255       ;     .byte  255       ; // + 9, 1: Product revision(8bit)     .byte  255,255,255,255 ; // +10, 4: Product serial number(32bit)     .byte  255       ; // +14, 1: Manufacturing date(8bit)     .byte  255       ; // +15, 1: 7-bit CRC checksum(7bit) "); void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ------------------------------------------------------------------------------------------- shortやintのフィールドを、人手で1バイトづつ分解して定義するというのは全く現実的ではありませんが、 Cコンパイラがアセンブラコードを自動生成する分には、やってくれても良さそうに思えますが.... たぶん、EPSONコンパイラはこのようなケースを想定していなかったのだと思います。 余談ですが、Linux PC上でGCCを使った場合は、構造体アライメント無しの場合に次のようなアセンブラコードが生成されます。 cid:     .byte  -1     .value -1     .byte  -1     .byte  -1     .byte  -1     .byte  -1     .byte  -1     .byte  -1     .byte  -1     .long  -1     .byte  -1     .byte  -1 最初に見た、P/ECE上での誤った例と同じに見えますが、PCの場合はこれで大丈夫なのです。 Linux PCのアセンブラはP/ECEのアセンブラと違って、初期状態では暗黙のアライメントを行わないみたいです。 ■■■■■■■■■■■■■■■■■■■■ ■構造体パックを使っても大丈夫なケース■ ■■■■■■■■■■■■■■■■■■■■ 以上のように、P/ECE開発環境では__attribute__((packed))による構造体パックがかなり大きな問題を抱えていることがわかりました。 通常、「使わないほうが良い」のは間違いありません。 が、まったく使えないかというとそうでもなくて、よく注意して使えば役に立つケースもあります。 かなり細かい話になりますので、必要なければ読み飛ばしてください。 �―藉�済み構造体を使わないケース ひとつめは、初期化済み構造体を使わないことが確実にわかっているケースです。 P/ECE上で正しく動作しなかったC言語プログラムを、初期化済み構造体を使わないよう、次のように書き換えてみました。 [構造体アライメント無し、初期化済み構造体を使わない] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 MMC_CID_INFO cid; void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   //構造体フィールドに値をひとつづつ格納   cid.ucMID = 0xff;   cid.usOID = 0xffff;   cid.ucPNM[0] = 0xff;   cid.ucPNM[1] = 0xff;   cid.ucPNM[2] = 0xff;   cid.ucPNM[3] = 0xff;   cid.ucPNM[4] = 0xff;   cid.ucPNM[5] = 0xff;   cid.ucPRV = 0xff;   cid.ulPSN = 0xffffffff;   cid.ucMDT = 0xff;   cid.ucCRC = 0xff;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ------------------------------------------------------------------------------------------- 正しい結果が得られました。 初期化済み構造体の場合はフィールドひとつひとつがデータ定義擬似命令にコンパイルされたため、前述のような暗黙のアライメント問題が発生したわけですが、 未初期化構造体の場合はフィールドひとつひとつに対応する擬似命令が生成されるわけではなく、次のようなアセンブラコードが生成されます。 .comm  cid 16  ; 16バイトのグローバル変数領域を確保せよ、という意味 構造体フィールドに値をひとつづつ格納する部分のC言語プログラムは、次のようにコンパイルされます。(読む必要ないです(^^;) xld.w  %r0,cid xld.w  %r10,0x000000ff xld.b  [%r0],%r10     ; + 0, 1: ManufactureID(8bit) xld.ub  %r11,[%r0+1]    ; + 1, 2: OEM/Application ID(16bit) (手順に無駄がありますが、間違いではないです) or    %r11,%r10 xld.b  [%r0+1],%r11 xld.ub  %r11,[%r0+2] or    %r11,%r10 xld.b  [%r0+2],%r11 xld.b  [%r0+3],%r10    ; + 3, 6: Product name(48bit) xld.b  [%r0+4],%r10 xld.b  [%r0+5],%r10 xld.b  [%r0+6],%r10 xld.b  [%r0+7],%r10 xld.b  [%r0+8],%r10 xld.b  [%r0+9],%r10    ; + 9, 1: Product revision(8bit) xld.ub  %r11,[%r0+10]    ; +10, 4: Product serial number(32bit) (手順に無駄がありますが、間違いではないです) or    %r11,%r10 xld.b  [%r0+10],%r11 xld.ub  %r11,[%r0+11] or    %r11,%r10 xld.b  [%r0+11],%r11 xld.ub  %r11,[%r0+12] or    %r11,%r10 xld.b  [%r0+12],%r11 xld.ub  %r11,[%r0+13] or    %r11,%r10 xld.b  [%r0+13],%r11 xld.b  [%r0+14],%r10    ; +14, 1: Manufacturing date(8bit) xadd   %r1,%r0,15     ; +15, 1: 7-bit CRC checksum(7bit) xld.b  [%r1],%r10 Cコンパイラは、パックされた構造体の各フィールドの正しいオフセットを認識しているので、正しいコードが生成されています。(ちょっと無駄ありますけれど) いま上で示したプログラム例のような、構造体の各フィールドをプログラムでひとつづつ初期化するケースに加えて、 ファイルを読み込んだりデバイスからバイト列を受信して、そのまま構造体変数に格納するケースでも、初期化済み構造体を使う必要がないのでOKです。 ��構造体末尾のパディングだけを除去する場合 もともと構造体アライメントありの場合にもフィールド間にはパディングがなくって、構造体末尾だけにパディングがあるような場合に、 構造体末尾のパディングを取り除いてそのぶん構造体サイズを小さくしたいケースで、__attribute__((packed))が利用可能です。 そんな都合の良い話があるか!と言われそうですが(^^;、あります。 USB仕様の構造体です。 USB仕様の構造体は、各フィールドのオフセットに関してはパディング無しで適切なアライメントになるよう定められていることが多いのですけれど、 なぜか末尾に1バイトのフィールドがあったりして、構造体パック無しでは末尾にパディングが付いてしまうようなケースがよくあるみたいなのです。 たとえば、USB通信端点の特性を記述するための構造体として、エンドポイントディスクリプタという構造体があります。 typedef struct _ENDPOINT_DESCRIPTOR {   unsigned char bLength;          /* + 0, 1 */   unsigned char bDescriptorType;      /* + 1, 1 */   unsigned char bEndpointAddress;      /* + 2, 1 */   unsigned char bAttributes;        /* + 3, 1 */   unsigned short wMaxPacketSize;       /* + 4, 2 */   unsigned char bInterval;         /* + 6, 1 */ } __attribute__((packed)) ENDPOINT_DESCRIPTOR; /* = 7 */ USBデバイス(P/ECE側です)は、デバイスの通信特性をホスト(PC側です)へ知らせるために、エンドポイントディスクリプタをデバイスからホストへ送信します。 エンドポイントディスクリプタの内容が変化することはまずないので、初期化済み構造体としてあらかじめ用意しておきます。 初期化済みエンドポイントディスクリプタをデバイスからホストへ送信するプログラムは、だいたい次のようなかんじになります。 const ENDPOINT_DESCRIPTOR endpoint_desc_ep2in = {   sizeof(ENDPOINT_DESCRIPTOR),  //unsigned char bLength;      /* + 0, 1 */   ENDPOINT_DESCRIPTOR_TYPE,    //unsigned char bDescriptorType;  /* + 1, 1 */   0x82,              //unsigned char bEndpointAddress;  /* + 2, 1 */   2,               //unsigned char bAttributes;    /* + 3, 1 */   EP2_NONISO_PACKET_SIZE,     //unsigned short wMaxPacketSize;   /* + 4, 2 */   0,               //unsigned char bInterval;     /* + 6, 1 */ }; D12_WriteEndpoint(1/*出力端点の指定*/, &endpoint_desc_ep2in, sizeof(ENDPOINT_DESCRIPTOR)); ※本当はエンドポイントディスクリプタだけを単体で送信することはないのですけれど、説明を簡単にするために、ここでは考えないことにします。 以上は、正しく動作するプログラムの例です。 さて、もしもENDPOINT_DESCRIPTORの定義のところで__attribute__((packed))を指定しなかったらどうなるでしょうか? typedef struct _ENDPOINT_DESCRIPTOR {   unsigned char bLength;          /* + 0, 1 */   unsigned char bDescriptorType;      /* + 1, 1 */   unsigned char bEndpointAddress;      /* + 2, 1 */   unsigned char bAttributes;        /* + 3, 1 */   unsigned short wMaxPacketSize;       /* + 4, 2 */   unsigned char bInterval;         /* + 6, 1 */   (パディング)                /* + 7, 1 */ } ENDPOINT_DESCRIPTOR;             /* = 8 */ const ENDPOINT_DESCRIPTOR endpoint_desc_ep2in = {   sizeof(ENDPOINT_DESCRIPTOR),  //unsigned char bLength;      /* + 0, 1 */ ... 本当は7とすべきなのに、8が設定されてしまう!!   ENDPOINT_DESCRIPTOR_TYPE,    //unsigned char bDescriptorType;  /* + 1, 1 */   0x82,              //unsigned char bEndpointAddress;  /* + 2, 1 */   2,               //unsigned char bAttributes;    /* + 3, 1 */   EP2_NONISO_PACKET_SIZE,     //unsigned short wMaxPacketSize;   /* + 4, 2 */   0,               //unsigned char bInterval;     /* + 6, 1 */                   //(パディング)            /* + 7, 1 */ }; D12_WriteEndpoint(1/*出力端点の指定*/, &endpoint_desc_ep2in, sizeof(ENDPOINT_DESCRIPTOR)); // 本当は D12_WriteEndpoint(1, &endpoint_desc_ep2in, 7) とすべきなのに、 // D12_WriteEndpoint(1, &endpoint_desc_ep2in, 8) になってしまう!! bLengthフィールドの値や送信バイト数が正しくないため、このプログラムは正しく動作しません。 bLengthフィールドや送信バイト数として、sizeof(ENDPOINT_DESCRIPTOR)を使うのではなく、明示的に"7"と書けば解決するのですが、 ひとつひとつの構造体サイズを覚えるよりも、できるならばsizeofが使えたほうが楽ですよね。 ちょっと細かい話になりますが、通常、エンドポイントディスクリプタは複数定義します。 が、その際、初期化済みエンドポイントディスクリプタ構造体を、配列にしてはいけません。 エンドポイントディスクリプタ構造体は、単体ならばパディング無しで適切なアライメントになっているのですけれど、 配列にすると二つ目の要素の途中でアライメントが崩れ、アセンブラによる暗黙のアライメントが行われて、不要なパディングが入ってしまうからです。 const ENDPOINT_DESCRIPTOR endpoint_desc[2] = { {   sizeof(ENDPOINT_DESCRIPTOR),  //unsigned char bLength;      /* + 0, 1 */   ENDPOINT_DESCRIPTOR_TYPE,    //unsigned char bDescriptorType;  /* + 1, 1 */   0x82,              //unsigned char bEndpointAddress;  /* + 2, 1 */   2,               //unsigned char bAttributes;    /* + 3, 1 */   EP2_NONISO_PACKET_SIZE,     //unsigned short wMaxPacketSize;   /* + 4, 2 */   0,               //unsigned char bInterval;     /* + 6, 1 */ }, {   sizeof(ENDPOINT_DESCRIPTOR),  //unsigned char bLength;      /* + 7, 1 */   ENDPOINT_DESCRIPTOR_TYPE,    //unsigned char bDescriptorType;  /* + 8, 1 */   0x02,              //unsigned char bEndpointAddress;  /* + 9, 1 */   2,               //unsigned char bAttributes;    /* +10, 1 */   EP2_NONISO_PACKET_SIZE,     //unsigned short wMaxPacketSize;   /* +11, 2 */   0,               //unsigned char bInterval;     /* +13, 1 */ } };                                   /* =14 */ ↓コンパイルすると...   .align 2 endpoint_desc:   .byte  7    //unsigned char bLength;      /* + 0, 1 */   .byte  5    //unsigned char bDescriptorType;  /* + 1, 1 */   .byte 130    //unsigned char bEndpointAddress;  /* + 2, 1 */   .byte  2    //unsigned char bAttributes;    /* + 3, 1 */   .half  64    //unsigned short wMaxPacketSize;   /* + 4, 2 */   .byte  0    //unsigned char bInterval;     /* + 6, 1 */   .byte  7    //unsigned char bLength;      /* + 7, 1 */   .byte  5    //unsigned char bDescriptorType;  /* + 8, 1 */   .byte  2    //unsigned char bEndpointAddress;  /* + 9, 1 */   .byte  2    //unsigned char bAttributes;    /* +10, 1 */            //(ここにパディングが入ってしまう!!) /* +11, 1 */   .half  64    //unsigned short wMaxPacketSize;   /* +12, 2 */ ずれてる!!   .byte  0    //unsigned char bInterval;     /* +14, 1 */ ずれてる!! 複数のエンドポイントディスクリプタを初期化済み構造体として定義する場合は、配列にするのでなく、ひとつひとつ定義し、 実行時にくっつけて送信する必要があります。 const ENDPOINT_DESCRIPTOR endpoint_desc_ep2in = { sizeof(ENDPOINT_DESCRIPTOR), ENDPOINT_DESCRIPTOR_TYPE, 0x82, 2, EP2_NONISO_PACKET_SIZE, 0, }; const ENDPOINT_DESCRIPTOR endpoint_desc_ep2out = { sizeof(ENDPOINT_DESCRIPTOR), ENDPOINT_DESCRIPTOR_TYPE, 0x02, 2, EP2_NONISO_PACKET_SIZE, 0, }; memcpy(&tmpbuf[sizeof(ENDPOINT_DESCRIPTOR) * 0], &endpoint_desc_ep2in, sizeof(ENDPOINT_DESCRIPTOR)); memcpy(&tmpbuf[sizeof(ENDPOINT_DESCRIPTOR) * 1], &endpoint_desc_ep2in, sizeof(ENDPOINT_DESCRIPTOR)); D12_WriteEndpoint(1/*出力端点の指定*/, &tmpbuf, sizeof(ENDPOINT_DESCRIPTOR) * 2); ■■■■■ ■まとめ■ ■■■■■ 結論です。 『P/ECE開発環境では、__attribute__((packed))による構造体パックは利用不可です。』 確実に問題が回避できると確信できる場合に限って、__attribute__((packed))も利用可能ですが、 それはまるで、生のアセンブラやバイナリエディタを使ってデータを操作しているような感覚です。 もともと#pargma packや__attribute__((packed))は処理系依存の拡張機能ですし、使わずに済むならばそれに越したことはありません。 * Sat Nov 6 00:00:00 JST 2004 Naoyuki Sawa - 構造体パックに問題あり#1 P/ECE開発環境のCコンパイラには問題があり、構造体パックを行う場合に注意が必要です。 今回は、この問題について記しておこうと思います。 P/ECE特有の問題点だけ読みたいかたは、■P/ECEの場合〜問題発生!!■まで読み飛ばしてください。 ■■■■■■■■■■■■■■■■■ ■一般的な話〜構造体アライメント■ ■■■■■■■■■■■■■■■■■ 通常、C言語で構造体を定義すると、各フィールドがフィールドサイズの倍数の位置に揃うように、暗黙の空白領域が追加されます。 たとえば次のような構造体を定義したとします。 typedef struct _SAMPLE1 {   char c1;   short s2;   int i3;   char c4; } SAMPLE1; これだけ見ると、各フィールドのオフセットとサイズ、および構造体サイズは次のようになるように見えるのですけれど、 (コメント表記は /* +フィールドオフセット, フィールドサイズ */、/* =構造体サイズ */ です。) [誤った配置] typedef struct _STRUCT1 {   char c1;  /* + 0, 1 */   short s2;  /* + 1, 2 */   short s3;  /* + 3, 2 */   int i4;  /* + 5, 4 */   char c5;  /* + 9, 1 */ } STRUCT1;    /* =10 */ 実際には、各フィールドのオフセットとサイズ、および構造体サイズは次のようになります。 [本当の配置] typedef struct _STRUCT1 {   char c1;  /* + 0, 1 */   (パディング) /* + 1, 1 */   short s2;  /* + 2, 2 */   short s3;  /* + 4, 2 */   (パディング) /* + 6, 2 */   int i4;  /* + 8, 4 */   char c5;  /* +12, 1 */   (パディング) /* +13, 3 */ } STRUCT1;    /* =16 */ s2フィールド(2バイト整数)に注目してください。 [誤った配置]では、s2フィールドがオフセット1(2の倍数でない)から始まっています。 2の倍数でないアドレスに置かれた2バイト整数の読み書きは、CPUの種類によってはできなかったり、できても効率が悪かったりします。 そこで、2バイト整数が2の倍数のアドレスから始まるよう、s2フィールドの直前に暗黙の空白領域が追加されるのです。 暗黙の空白領域のことを、“パディング”(詰め物)と呼んだりします。 i4フィールド(4バイト整数)についても同様です。 4の倍数でないアドレスに置かれた4バイト整数の読み書きは、CPUの種類によってはできなかったり、できても効率が悪かったりします。 そこで、4バイト整数が4の倍数のアドレスから始まるよう、i4フィールドの直前にパディングが追加されます。 一見、構造体末尾のパディングは不要に見えます。 なぜこれが必要かというと、構造体配列を定義したときのためです。 もしも構造体末尾の空白領域がなかったとすると、次のような構造体配列を定義したときに、 STRUCT1 struct1[2]; メモリ上の配置は次のようになってしまいます。 [誤った配置] // struct1[0] char c1;  /* + 0, 1 */ (パディング) /* + 1, 1 */ short s2;  /* + 2, 2 */ short s3;  /* + 4, 2 */ (パディング) /* + 6, 2 */ int i4;  /* + 8, 4 */ char c5;  /* +12, 1 */ // struct1[1] char c1;  /* +13, 1 */ (パディング) /* +14, 1 */ short s2;  /* +15, 2 */ short s3;  /* +17, 2 */ (パディング) /* +19, 2 */ int i4;  /* +21, 4 */ char c5;  /* +25, 1 */ struct1[0]は適切なフィールド配置ですが、struct1[1]のs2やs3、i4の配置が適切でありません。 s2とs3は2バイト整数なのに2の倍数アドレスに配置されず、i4は4バイト整数なのに4の倍数アドレスに配置されていません。 このような問題を避けるため、構造体サイズが「構造体の中のいちばん大きなフィールドのサイズの倍数」になるよう、構造体の末尾にもパディングが追加されます。 [正しい配置] // struct1[0] char c1;  /* + 0, 1 */ (パディング) /* + 1, 1 */ short s2;  /* + 2, 2 */ short s3;  /* + 4, 2 */ (パディング) /* + 6, 2 */ int i4;  /* + 8, 4 */ char c5;  /* +12, 1 */ (パディング) /* +13, 3 */ // struct1[1] char c1;  /* +16, 1 */ (パディング) /* +17, 1 */ short s2;  /* +18, 2 */ short s3;  /* +20, 2 */ (パディング) /* +22, 2 */ int i4;  /* +24, 4 */ char c5;  /* +28, 1 */ (パディング) /* +29, 3 */ 以上のように、コンパイラが自動的にパディングを追加して、各フィールドの配置を適切に保つことを、“構造体アライメント”と呼んだりします。 ■■■■■■■■■■■■■■■■■■■■■■ ■一般的な話〜構造体アライメントを抑制する■ ■■■■■■■■■■■■■■■■■■■■■■ 通常は、コンパイラが自動的に構造体アライメントを行ってくれるので、プログラマは特に気にする必要はありません。 が、稀に、構造体アライメントが行われては困る場合があります。 ネットワークやデバイスとの通信など、あらかじめ仕様で決められたフィールド配置の構造体を送受信する場合です。 ネットワークやデバイスとの通信では、通信データ量を少なくするために、2バイト整数や4バイト整数もパディング無しでびっちり詰め込まれている場合が多いです。 メモリ上に構造体を作っておいて、そのメモリイメージを直接送信する場合や、メモリ上の構造体変数に直接受信する場合、 構造体のフィールド配置はあらかじめ決められた仕様にきっちり合っていなければいけません。 たとえば、MMC(Multi Media Card)との通信では、次のような構造体の仕様が定められています。 (まどかの世界さんの、MMC対応カーネルのソースから引用させていただきました) [正しい配置] //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 1, 2: OEM/Application ID(16bit)   unsigned char ucPNM[6];  // + 3, 6: Product name(48bit)   unsigned char ucPRV;   // + 9, 1: Product revision(8bit)   unsigned long ulPSN;   // +10, 4: Product serial number(32bit)   unsigned char ucMDT;   // +14, 1: Manufacturing date(8bit)   unsigned char ucCRC;   // +15, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =16 ところが、コンパイラによって構造体アライメントが行われると、実際の配置は次のようになってしまいます。 [誤った配置] //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char ucMID;   // + 0, 1: ManufactureID(8bit)   (パディング) // + 1, 1:   unsigned short usOID;   // + 2, 2: OEM/Application ID(16bit)   unsigned char ucPNM[6];  // + 4, 6: Product name(48bit)   unsigned char ucPRV;   // +10, 1: Product revision(8bit)   (パディング) // +11, 1:   unsigned long ulPSN;   // +12, 4: Product serial number(32bit)   unsigned char ucMDT;   // +16, 1: Manufacturing date(8bit)   unsigned char ucCRC;   // +17, 1: 7-bit CRC checksum(7bit)   (パディング) // +18, 2: } MMC_CID_INFO;         // =20 これではダメです。 そこで、コンパイラによる構造体アライメントを抑制し、パディングをなくす方法が用意されています。 構造体アライメントを抑制する方法は、コンパイラ毎に違っています。 たとえば、Visual C++の場合は、次のようにします。 #pragma pack(1) //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 1, 2: OEM/Application ID(16bit)   unsigned char ucPNM[6];  // + 3, 6: Product name(48bit)   unsigned char ucPRV;   // + 9, 1: Product revision(8bit)   unsigned long ulPSN;   // +10, 4: Product serial number(32bit)   unsigned char ucMDT;   // +14, 1: Manufacturing date(8bit)   unsigned char ucCRC;   // +15, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =16 #pragma pack() GCCの場合は、次のようにします。 //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 ※厳密に言うと、MMCとの通信データはバイト順がモトローラ並びで、PentiumやS1C33(P/ECEのCPU)のインテル並びとは逆です。  だから、構造体アライメントだけ無くしても、メモリイメージそのままで送受信することはできません。  が、説明を簡単にするために、ここではバイト順の違いは考えないことにしてください。 それでは、本当に構造体アライメントが抑制されているか、確認してみましょう。 テストのために、メモリ上に初期化済みの構造体を用意し、その内容を1バイトづつ表示してみることにします。 まずは、構造体アライメントを★抑制しない★場合のメモリ内容を確かめてみます。 [Windows PC & Visual C++ 6.0、構造体アライメントあり] #include //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 2, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];  // + 4, 6: Product name(48bit)   unsigned char  ucPRV;   // +10, 1: Product revision(8bit)   unsigned long  ulPSN;   // +12, 4: Product serial number(32bit)   unsigned char  ucMDT;   // +16, 1: Manufacturing date(8bit)   unsigned char  ucCRC;   // +17, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =20 MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // +10, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit) }; int main() {   int i;   unsigned char* p;   p = (unsigned char*)&cid;   printf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     printf("%02x ", p[i]);   }   return 0; } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 20 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ff ff 00 00 ------------------------------------------------------------------------------------------- 構造体アライメントありなので、パディングが追加されています。(00の部分) 構造体サイズもパディングの分増えて、MMC仕様よりも4バイト多い20バイトになっています。 次に、構造体アライメントを★抑制した★場合のメモリ内容を確かめてみます。 [Windows PC & Visual C++ 6.0、構造体アライメント無し] #include #pragma pack(1) //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];  // + 3, 6: Product name(48bit)   unsigned char  ucPRV;   // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;   // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;   // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;   // +15, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =16 #pragma pack() MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 1, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 3, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // + 9, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +10, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +14, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +15, 1: 7-bit CRC checksum(7bit) }; int main() {   int i;   unsigned char* p;   p = (unsigned char*)&cid;   printf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     printf("%02x ", p[i]);   }   return 0; } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ------------------------------------------------------------------------------------------- パディングが消えて、構造体サイズもMMC仕様どおりの16バイトになりました。 GCCの場合も同様です。 まずは、構造体アライメントを★抑制しない★場合。 [Linux PC & GCC 3.3.3、構造体アライメントあり] #include //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 2, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];  // + 4, 6: Product name(48bit)   unsigned char  ucPRV;   // +10, 1: Product revision(8bit)   unsigned long  ulPSN;   // +12, 4: Product serial number(32bit)   unsigned char  ucMDT;   // +16, 1: Manufacturing date(8bit)   unsigned char  ucCRC;   // +17, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =20 MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // +10, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit) }; int main() {   int i;   unsigned char* p;   p = (unsigned char*)&cid;   printf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     printf("%02x ", p[i]);   }   return 0; } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 20 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ff ff 00 00 ------------------------------------------------------------------------------------------- 次に、構造体アライメントを★抑制した★場合。 [Linux PC & GCC 3.3.3、構造体アライメント無し] #include //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 1, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 3, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // + 9, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +10, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +14, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +15, 1: 7-bit CRC checksum(7bit) }; int main() {   int i;   unsigned char* p;   p = (unsigned char*)&cid;   printf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     printf("%02x ", p[i]);   }   return 0; } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ------------------------------------------------------------------------------------------- 期待通りです。 構造体アライメントを抑制することを、“構造体パック”と呼んだりします。 ■■■■■■■■■■■■■■■■ ■P/ECEの場合〜問題発生!!■ ■■■■■■■■■■■■■■■■ 上述のGCCの例は、PC上でLinux(FedoraCore2)を使って行いました。 さて、P/ECE開発環境のCコンパイラもGCCです。 ちょっとバージョンが古いけど、PC上でLinuxを使った場合と同じ結果が期待されます。 まずは、構造体アライメントを★抑制しない★場合。 [P/ECE & pcc33.exe、構造体アライメントあり] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;   // + 0, 1: ManufactureID(8bit)   unsigned short usOID;   // + 2, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];  // + 4, 6: Product name(48bit)   unsigned char  ucPRV;   // +10, 1: Product revision(8bit)   unsigned long  ulPSN;   // +12, 4: Product serial number(32bit)   unsigned char  ucMDT;   // +16, 1: Manufacturing date(8bit)   unsigned char  ucCRC;   // +17, 1: 7-bit CRC checksum(7bit) } MMC_CID_INFO;         // =20 MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 2, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 4, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // +10, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +12, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +16, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +17, 1: 7-bit CRC checksum(7bit) }; void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 20 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ff ff 00 00 ------------------------------------------------------------------------------------------- あとは、構造体アライメントを★抑制した★場合が期待通りの結果ならば、全てOKなのですが・・・ [P/ECE & pcc33.exe、構造体アライメント無し] #include unsigned char vbuff[128 * 88]; //CID(Card IDentification number) カードIDレジスタ情報構造体 typedef struct tag_MMC_CID_INFO {   unsigned char  ucMID;       // + 0, 1: ManufactureID(8bit)   unsigned short usOID;       // + 1, 2: OEM/Application ID(16bit)   unsigned char  ucPNM[6];      // + 3, 6: Product name(48bit)   unsigned char  ucPRV;       // + 9, 1: Product revision(8bit)   unsigned long  ulPSN;       // +10, 4: Product serial number(32bit)   unsigned char  ucMDT;       // +14, 1: Manufacturing date(8bit)   unsigned char  ucCRC;       // +15, 1: 7-bit CRC checksum(7bit) } __attribute__((packed)) MMC_CID_INFO; // =16 MMC_CID_INFO cid = {   0xff,                  // unsigned char  ucMID;  // + 0, 1: ManufactureID(8bit)   0xffff,                 // unsigned short usOID;  // + 1, 2: OEM/Application ID(16bit)   { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, // unsigned char  ucPNM[6]; // + 3, 6: Product name(48bit)   0xff,                  // unsigned char  ucPRV;  // + 9, 1: Product revision(8bit)   0xffffffff,               // unsigned long  ulPSN;  // +10, 4: Product serial number(32bit)   0xff,                  // unsigned char  ucMDT;  // +14, 1: Manufacturing date(8bit)   0xff,                  // unsigned char  ucCRC;  // +15, 1: 7-bit CRC checksum(7bit) }; void pceAppInit() {   pceLCDSetBuffer(vbuff);   pceLCDDispStart(); } void pceAppProc(int count) {   int i;   unsigned char* p;   memset(vbuff, 0, sizeof vbuff);   pceFontSetType(2);   pceFontSetPos(0, 0);   p = (unsigned char*)&cid;   pceFontPrintf("size = %d\n", sizeof(MMC_CID_INFO));   for(i = 0; i < sizeof(MMC_CID_INFO); i++) {     pceFontPrintf("%02x ", p[i]);   }   pceLCDTrans(); } void pceAppExit() { } ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff 00 ff ff ff ff ff ff ff ff ff 00 ff ff ff ff ------------------------------------------------------------------------------------------- 問題発生です!! 構造体サイズは確かに仕様どおりの16バイトになっているのですが、なぜかパディングが残っています。 わかりやすいように、結果出力を並び替えてみます。 ------------------------------------------------------------------------------------------- ■■ 結果 ■■ size = 16 ff         // + 0, 1: ManufactureID(8bit) 00         // + 1, 1: なぜかパディングが残っている? ff ff        // + 2, 2: OEM/Application ID(16bit) ff ff ff ff ff ff  // + 4, 6: Product name(48bit) ff         // +10, 1: Product revision(8bit) 00         // +11, 1: なぜかパディングが残っている? ff ff ff ff     // +12, 4: Product serial number(32bit) ??         // +??, 1: Manufacturing date(8bit) はどこへ行った? ??         // +??, 1: 7-bit CRC checksum(7bit) はどこへ行った? ------------------------------------------------------------------------------------------- いったいどうなっているのでしょう? この問題、P/ECE開発環境のCコンパイラ(gcc33.exe)とアセンブラ(as33.exe)の協調が上手くいっていないことが原因なのです。 詳しくは、次回に。 (続きます…) * Sat May 16 00:00:00 JST 2004 Naoyuki Sawa - Zlibを実装してみる#2 前回は、省メモリなZlib展開ルーチンを実装してみました。 また、そのルーチンを使ってZlib形式の圧縮ファイルを展開し、画面表示するサンプルを作成しました。 しかしながら、Zlib形式の圧縮ファイルは、そのままでは少々使いづらい面があります。 いちばんの問題は、Zlib形式に対応した圧縮・展開ツールが普及していない、という点です。 Zlibの仕様書によると、実は「Zlib形式」とは、圧縮方法まで含めた仕様ではないそうです。 圧縮方法を示したヘッダと、圧縮データ本体、そして検査用コードの並びを既定しているだけの仕様です。 図で示すと、こんな感じです。 +===========================+ |Zlibヘッダ                    |←Zlib仕様 +−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |Zlibヘッダに書いてある圧縮方法を使った、圧縮データ| +−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |検査用コード                     |←Zlib仕様 +===========================+ Windows WAVファイル形式が、PCM形式・ADPCM形式・MP3形式など、いろいろな内部形式を持っているのに似ています。 さて、現在のところZlib形式で利用可能な圧縮方法としては、「Deflate圧縮形式」だけが規定されています。 一般にZlib形式と呼ばれるのは、厳密には「Deflate圧縮形式の圧縮データを持った、Zlib形式のファイル」のことだったのです。 もちろん、前回実装したZlib展開ルーチンも、Deflate圧縮形式に基くものです。 先ほどの図を書き直すと、次のようになります。 +===============+ |Zlibヘッダ        |←Zlib仕様 +−−−−−−−−−−−−−−−+ |Deflate形式の圧縮データ| +−−−−−−−−−−−−−−−+ |検査用コード         |←Zlib仕様 +===============+ Zlib形式の圧縮ファイルを扱うには、圧縮・展開ツールがZlib形式のヘッダや検査用コードを認識できなければいけません。 複数の形式に対応した圧縮・展開ツールは様々ありますが、Zlib形式を扱うことのできるツールは見つけられませんでした。 僕は、こちらのサンプルプログラムをそのままZlib圧縮・展開ツールとして使わせて頂いています。 「Haruhiko Okumura's Home Page(http://www.matsusaka-u.ac.jp/~okumura/)」さんのサイトの、 「zlib 入門(http://www.matsusaka-u.ac.jp/~okumura/compression/zlib.html)」のページの、 「comptest.c(http://www.matsusaka-u.ac.jp/~okumura/compression/comptest.c)」。 コンパイルするためには、オリジナルのZlib(http://www.gzip.org/zlib/)が必要です。 さて、Zlib形式よりも圧倒的にメジャーな圧縮ファイル形式として、「Zip形式」があります。 Zip形式も、Zlib形式と同じく、圧縮方法までを定めた仕様ではありません。 様々な圧縮方法を使った圧縮データの格納方法や、ファイル情報を付加する方法等を定めた仕様です。 Zip形式が扱う“様々な圧縮方法”の中でも特に良く利用されるのが、前出の「Deflate圧縮形式」です。 同じDeflate圧縮形式を使ったZlib形式とZip形式は、ガワが違うだけで本質はほとんど同じ、と言えます。 Zlib形式と違って、Zip形式は複数のファイルをまとめて格納できるので、図で示すとこんな感じになります。 +================================+ |Zipファイルヘッダ1、検査用コードも含む           |←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |Zipファイルヘッダ1に書いてある圧縮方法を使った、圧縮データ1| +================================+ |Zipファイルヘッダ2、検査用コードも含む           |←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |Zipファイルヘッダ2に書いてある圧縮方法を使った、圧縮データ2| +================================+ |               ・                | |               ・                | |               ・                | +================================+ |ZipファイルヘッダN、検査用コードも含む           |←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+ |ZipファイルヘッダNに書いてある圧縮方法を使った、圧縮データN| +================================+ |その他のZipファイル情報                   |←Zip仕様 +================================+ あるZipファイル内の圧縮データが、全てDeflate圧縮形式を利用していたならば、先ほどの図は次のようになります。 +=====================+ |Zipファイルヘッダ1、検査用コードも含む|←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−+ |Deflate形式の圧縮データ1     | +=====================+ |Zipファイルヘッダ2、検査用コードも含む|←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−+ |Deflate形式の圧縮データ2     | +=====================+ |          ・          | |          ・          | +=====================+ |ZipファイルヘッダN、検査用コードも含む|←Zip仕様 +−−−−−−−−−−−−−−−−−−−−−+ |Deflate形式の圧縮データN     | +=====================+ |その他のZipファイル情報        |←Zip仕様 +=====================+ Zipファイルに格納する圧縮データの形式としては、Deflate圧縮形式以外にもいくつかの圧縮方法が規定されています。 しかし、手元にある多くのZipファイルを調べてみたところ、実際に使われていた圧縮方法は二種類だけでした。 ・無圧縮 ・Deflate圧縮形式 従って、これら二種類の圧縮形式に対応するだけで、実用上は充分なZip展開ルーチンとなるはずです。 無圧縮形式については、格納されているデータをそのままコピーするだけです。 Deflate圧縮形式については、前回作成したZlib展開ルーチンの内部処理が流用できそうです。 Zlibヘッダと検査コードを扱っている部分のプログラムを少し修正するだけで、Zip展開ルーチンを作成できました。 Zip展開ルーチンを利用したサンプルはこちら。 P/ECE用とWindows用のテストプログラムを用意しました。 P/ECE用テストプログラムは、Zipファイルに格納された画像ファイルを、次々自動的に表示します。 Windowsテストプログラム用は、Zipファイルに格納されたファイル一覧から、選択された画像を展開して表示します。 http://www.piece-me.org/archive/pzltest-20040316-piece.zip http://www.piece-me.org/archive/pzltest-20040316-win32.zip 前回と今回とで、Zlib/Zip展開ルーチンを実装してみました。 二年近く、よくわからないままに使っていたZlibライブラリでしたが、今回のプログラミングでやっと理解することができました。 Zlib/Zip形式の仕組みを理解するために、以下の記事がとても参考になりました。 お礼を申し上げるとともに、ご紹介させて頂きます。 「LabVIEW情報交換のページ(http://www.asahi-net.or.jp/~WR9K-OOHS/)」さんのサイトの、 「ナイジェル山口コーナー(http://www.asahi-net.or.jp/~WR9K-OOHS/Pages/Nigel's/nigel.html)」のページ。 Zlibの仕様書の、いちばん重要な部分に関する和訳があります。 また、圧縮技法の詳細について、わかりやすく論じられています。 圧縮技法以外にも、興味深い記事がたくさんあります。 「KONE's D−Station(http://kone.vis.ne.jp/)」さんのサイトの、 「気まぐれな戯れ言(http://kone.vis.ne.jp/diary/)」のページ。 CRCの計算方法が、順を追って解説されています。 Zip形式で使われているCRC-32の計算方法、多くの実装例はテーブルを利用していますが、そのテーブルはどこから出てくるのか? こちらのページを読んで疑問が解決しました。 今回の作成したZip展開ルーチンは、テーブルを利用しない基本的な方法で実装してみました。 * Sat May 13 16:00:00 JST 2004 Naoyuki Sawa - Zlibを実装してみる#1 2002年6月14日〜17日のP/ECE研究記録「zlibを使う#1〜3」にて、P/ECE用プログラムでZlibを使う方法を調べました。 その後、いくつかの自作プログラムで、実際にZlibを利用しました。 それらは一応上手く動作したのですが、P/ECE用プログラムではZlibが使いづらいと感じる点もいくつかありました。 いちばんの問題は、「zlibを使う#1〜3」でも書いたように、メモリ消費量が多すぎる点です。 圧縮データを格納しておく入力バッファ、展開データを作成するための出力バッファの他に、約35キロバイトの作業用メモリが必要です。 P/ECE用プログラムでは、実際に利用する圧縮データや展開データはさほど大きくありません。たいてい数キロバイト程度です。 それよりも、Zlib内部の一時的な作業用メモリのために、35キロバイトも必要なのがかなり厳しいところです。 Zlibが作業用メモリを確保できないと、展開処理が失敗してしまいます。 その後「P/ECE HAND BOOK」に、P/ECEカーネルソースの展開ルーチンを使う方法が掲載されました。 P/ECEカーネルソースの展開ルーチンも同じZlibですが、P/ECEのメモリ構成に特化していて、より少ないメモリで展開処理が可能です。 また、「P/ECE HAND BOOK」の方法を元にして、複数の圧縮ファイルをまとめて扱う方法が、 「てとら★ぽっと(http://www.zurachu.net/)」さんのサイトで紹介(http://www.zurachu.net/piece/tips/ppack.html)されています。 Zlibにこだわらなければ、より高速な圧縮・展開方法が「p/ware(http://park17.wakwak.com/~hitode/piece/index.html)」さんのサイトで 紹介(http://park17.wakwak.com/~hitode/piece/index.html#plz)されています。 これらの方法を使わせてもらえば実用上は問題解決なのですが、もう少しだけ素のZlibにこだわってみたいと思います。 そもそも僕がZlibの内部動作を理解しないまま使っていたのが問題なので、この機会に勉強してみようというわけです。 さて、なぜZlibはこんなに大きな作業用メモリを必要とするのでしょうか? Zlibの圧縮・展開アルゴリズムは、「ハフマン符号化」と「スライド辞書圧縮」に基づいています。 ハフマン符号化は、頻繁に現れる値に小さな符号、稀に現れる値に大きな符号を割り振って、全体のサイズを小さくする方法です。 ハフマン符号化の展開処理には、「ハフマン木」というデータ構造が利用されます。 「ハフマン木」は、ハフマン符号から元の値に変換するための辞書となります。 「ハフマン木」を作成するために、いくらかのメモリが必要です。 これが、Zlibが大きな作業用メモリを必要とする理由の一つ目です。 しかし実際には、Zlibで利用される「ハフマン木」はさほど大きなデータではありません。せいぜい数キロバイト程度です。 より大きなメモリを必要としているのは、スライド辞書圧縮アルゴリズムで利用される「スライド窓」データ構造です。 スライド辞書圧縮とは、同じパターンのデータ並びを距離と長さに置き換えて、全体のサイズを小さくする方法です。 例えば「012345678901234」というデータ並びがあった場合、次のような感じで圧縮します。 「0123456789、10文字前から、5文字繰り返し」 ところで、Zlibの実装は入力も出力も基本的にストリーミングです。 つまり、一度読み込んだデータは再び読み込めない、一度出力したデータはもう忘れた、というわけです。 しかし、10文字前のデータをもう一度出力するためには、10文字前に出力したデータを覚えておかなければいけません。 そこで、いくらかの出力バッファを用意し、過去何文字分かの出力履歴を記憶しておきます。 これを「スライド窓」と呼びます。 Zlibの仕様では、最大で「3万2千文字前から、258文字繰り返し」という指示が有り得ることになっています。 従って、過去3万2千文字分の出力履歴を記憶しておかなければいけません。 つまり、32キロバイトのスライド窓が必要となります。 これで疑問が解決しました。35キロバイトの作業用メモリの大部分は、スライド窓に使われていたのです。 さて、さきほど“Zlibの実装は入力も出力も基本的にストリーミング”だからスライド窓が必要、と書きました。 より限定すると、“出力がストリーミングだから”出力履歴を記憶しておくためにスライド窓が必要となります。 では、出力をストリーミングにする必要はあるのでしょうか? 僕が作ったP/ECE&Zlib使用のプログラムで、出力ストリーミングを活用したケースはたった一度だけでした。 「VGMプレイヤー」です。 VGMプレイヤーでは、VGZ形式で圧縮された演奏データを少しづつ展開しながら演奏処理を行いました。 しかしこれとて全展開データは充分P/ECEのメモリに収まる程度のサイズで、むしろ作業用メモリの方が大きいぐらいです。 つまり、P/ECE上でZlibの出力ストリーミングが必要となるケースはなかった、と言えます。 より大きな圧縮・展開データを扱う、PC上のプログラムではいくぶん事情も変わってくるのでしょうが、 少なくともゲームプログラムにおいては全データをオンメモリに展開するケースが大部分なのではないかと思います。 そんなわけで、どうやらZlibの出力ストリーミングは僕には必要ないようです。 Zlibの出力ストリーミングをとりやめて、作業用メモリの必要量を減らすことにしました。 さっそくZlibのソース改造にとりかかったのですが・・・難解です(^^; Zlib展開の手順は、さほど複雑なものではありません。 本家「The gzip home page(http://www.gzip.org/)」さんのサイトの、Zlib Home Site(http://www.gzip.org/zlib/)」 →「zlib Documentation(http://www.gzip.org/zlib/zlib_docs.html)」のページから、Zlibの仕様書が入手できます。 ハフマン木作成のところが多少トリッキーですけれど、それ以外は解り易くてシンプルな仕様です。 にも関わらずZlibのソースが難解なのは、移植性と高速化のため、プログラムが複雑化しているのが原因のようです。 特にハフマン復号処理の高速化に関しては相当に力が入っているようで、ソース全体のかなりの部分を占めています。 これらの複雑なルーチンに影響を与えないよう、出力ストリーミングだけを取りやめるのは、ちょっと危険な感じです。 そこで今回は、Zlibのソースを改造するのではなく、自前で再実装してみることにしました。 できあがったのがこちらです。 http://www.piece-me.org/archive/pzltest-20040313-piece.zip http://www.piece-me.org/archive/pzltest-20040313-win32.zip (ファイル内容は、README.TXTをご覧下さい。詳細は次回に。) ※2004/03/14追記: バグ修正および再調整しました。  この記事のいちばん最後に、更新版のURLがあります。 P/ECE用とWindows用のテストプログラムを用意してみました。 いずれもZlib圧縮された画像ファイルを展開し、画面表示するテストプログラムです。 テストプログラムはP/ECE用とWindows用で別々ですが、Zlib展開ルーチンのソースはほぼ共通となっています。 出力ストリーミングを取りやめたので、スライド窓が不要となり、作業用メモリは8キロバイト程度に減りました。 もっと減らしても大丈夫そうなのですが、それは今後の調整課題です。 オリジナルのZlibと異なり、移植性・高速化に関する部分はバッサリです。 移植性に関しては、僕は当面P/ECE、Windows、あとPC用のLinuxぐらいで動けば充分なので、ほとんど何も考えていません。 32ビット環境で、バイトオーダーもリトルエンディアンに決め打ちです。 高速化も全く行っていませんが、P/ECEで扱う程度のデータサイズならば、あまり違いは感じられないはずです。 大まかに言って、1秒当り10〜15キロバイトぐらいの展開速度だと思います。 徹底的に簡素化したので、プログラムサイズはヘッダとソース合わせて1000行程度に小さくなりました。 オリジナルのZlibも、もともとはこれぐらい簡素だったと思うのです。 長く使われていろいろな環境に移植され、また強力に高速化された結果、現在のように複雑な実装になったのでしょう。 今回実装したZlibルーチンは、言わば先祖還りのようなものです。 P/ECEぐらいの規模の環境には、これぐらい簡素な実装で充分なのではないかなぁ、と思いました。 ・・・と、実装してみてからP/ECEカーネルソースの展開ルーチンを読み返すと、図らずも似た感じになってますね。 考えることは同じですねぇ(^^; (続きます…) ---------------------------------------------------------------------------------------------------- 2004/03/14追記: ・バグ修正しました。  大きな圧縮データを展開すると、展開処理に失敗してしまっていたバグを修正しました。 ・作業用メモリサイズを調整しました。  昨日のバージョンでは、ハフマン木作成のために8キロバイトの作業用メモリを確保していました。  いくつかの圧縮データを試してみたところ、実際に使われているのはその1/6程度でした。  ちょっと余裕がありすぎるので、作業用メモリを半分の4キロバイトに減らしてみました。  まだ3倍程度の余裕がありますから、たぶん大丈夫だとは思いますが、ちょっと注意が必要です。  オリジナルのZlibでも、ハフマン木作成のための作業用メモリサイズは、実験で決めているようです。  作業用メモリの最大必要サイズは、理論で求められないみたいです。 ダウンロードはこちら: http://www.piece-me.org/archive/pzltest-20040314-piece.zip http://www.piece-me.org/archive/pzltest-20040314-win32.zip ---------------------------------------------------------------------------------------------------- * Tue Dec 23 08:00:00 JST 2003 Naoyuki Sawa - ディレイド分岐命令の誤動作#4 (前回からの続き…) 引き続き、ディレイド分岐命令の直前にメモリアクセス命令を置いた場合の、誤動作の有無を実験します。 前々回は「jp.d %rb」、前回は「call.d %rb」「ret.d」について調べました。 今回は、残りの「jp.d sign8」「jr.d sign8」「call.d sign8」について調べます。     ○ディレイド分岐命令      1. jp.d sign8      2. jr.d sign8      3. call.d sign8     ○メモリアクセス命令      1. ld.b [%rb], %rs      2. ld.ub [%rb], %rs      3. ld.h [%rb], %rs      4. ld.uh [%rb], %rs      5. ld.w [%rb], %rs      6. ld.b %rd, [%rb]      7. ld.h %rd, [%rb]      8. ld.w %rd, [%rb]     ○分岐元・分岐先      1. 高速RAM      2. SRAM     ○メモリアクセス先      1. 高速RAM      2. SRAM 前回までと異なり、分岐元と分岐先のメモリの種類が違うパターンについては実験していません。 「jp.d sign8」「jr.d sign8」「call.d sign8」は前後256バイトまでの範囲にしか分岐できないため、 高速RAM(0x0000〜0x1fff)からSRAM(0x100000〜0x13ffff)へ、あるいはSRAMから高速RAMへは、遠すぎて分岐できないからです。 (「ext」命令と組み合わせれば前後256バイト以上の範囲に分岐できますが、メモリアクセス命令とディレイド分岐命令の間に「ext」命令が入ります。  ディレイド分岐命令の直前にメモリアクセス命令がある状況だけをテストしているので、「ext」命令を挟むケースは今回は除外することにしました。) 検証用プログラムはこちら:     「jp.d sign8」    http://www.piece-me.org/archive/delyerr4-20031223.zip     「jr.d sign8」 http://www.piece-me.org/archive/delyerr5-20031223.zip     「call.d sign8」   http://www.piece-me.org/archive/delyerr6-20031223.zip 検証用プログラムの結果出力ファイル(生のCSV形式)はこちら:     「jp.d sign8」    http://www.piece-me.org/piece-lab/delyerr4-20031223-result.csv     「jr.d sign8」 http://www.piece-me.org/piece-lab/delyerr5-20031223-result.csv     「call.d sign8」   http://www.piece-me.org/piece-lab/delyerr6-20031223-result.csv 出力結果ファイルを読みやすいように加工してみたのがこちら:     「jp.d sign8」    http://www.piece-me.org/piece-lab/delyerr4-20031223-kekka.csv     「jr.d sign8」 http://www.piece-me.org/piece-lab/delyerr5-20031223-kekka.csv     「call.d sign8」   http://www.piece-me.org/piece-lab/delyerr6-20031223-kekka.csv 結果、     --------------------------------     全ての組み合わせで、誤動作無し!     -------------------------------- 「jp.d sign8」「jr.d sign8」「call.d sign8」はいずれも非常に使用頻度の高いディレイド分岐命令で、これまでにも多用しています。 これまでの使用において特に問題は発生していなかったので、誤動作条件が存在しないのも予想通りでした。 -------------------------------------------------------------------------------------------------- 以上、四回に渡って、ディレイド分岐命令の誤動作条件を調べてきました。 まだ全てのメモリアクセス命令や全ての条件の組み合わせを試したわけではありませんが、とりあえずここで総括してみます。 結局、マニュアルに記載されていない誤動作条件が存在するディレイド分岐命令は、「jp.d %rb」だけでした。 ディレイド分岐命令を使ってプログラムを最適化する場合に、注意しなければいけないルールは次の通りです:     『「jp.d %rb」命令の直前にメモリアクセス命令を置いてはいけない』 とりあえずこのルールに従っておけば、誤動作することはないと思います。 より詳しい誤動作条件については、前々回の研究記録『ディレイド分岐命令の誤動作#2』をご参照ください。 このルール、S1C33209 CPUのマニュアルには載っていません。 マニュアルの記載漏れか、あるいは想定外の誤動作なのか・・・いずれにせよ、危険度「大」です。 * Mon Dec 22 22:00:00 JST 2003 Naoyuki Sawa - ディレイド分岐命令の誤動作#3 (前回からの続き…) 前回は、ディレイド分岐命令「jp.d %rb」の直前にメモリアクセス命令を置いた場合の、誤動作の有無を実験しました。 その結果、いくつかの組み合わせで誤動作することがわかり、 『「jp.d %rb」命令の直前にメモリアクセス命令を置いてはいけない』 という結論が得られました。 今回は、ディレイド分岐命令「call.d %rb」と「ret.d」について、同様に実験してみることにします。 前回実験した「jp.d %rb」命令、実はディレイド分岐命令の中ではいちばん使用頻度が低いです。 「jp %rb」を使う場面はテーブルジャンプ処理ぐらいしかなく、C言語ではswitch文に相当します。 switch文の実行性能がプログラム全体の性能に大きく影響するようなケースは、比較的少ないと思います。 従って、「jp %rb」を「jp.d %rb」に置き換えてまで処理速度を稼ぐような場面も、あまりないと思うからです。 「jp.d %rb」に較べて、「call.d %rb」と「ret.d」の使用頻度ははるかに高いです。 「call.d %rb」は関数ポインタ呼び出しに限らず、頻繁に使う関数のアドレスをレジスタに保持するようなケースで頻出します。 「ret.d」は・・・実は僕はあまり使っていないのですが、EPSON社純正ライブラリの除算ルーチンがこれを有効活用しています。 以前の研究記録『実はディレイド命令』の回をご参照ください。 というわけで、「call.d %rb」「ret.d」の直前にメモリアクセス命令を置いた場合の、誤動作の有無を実験してみました。     ○ディレイド分岐命令      1. call.d %rb      2. ret.d     ○メモリアクセス命令      1. ld.b [%rb], %rs      2. ld.ub [%rb], %rs      3. ld.h [%rb], %rs      4. ld.uh [%rb], %rs      5. ld.w [%rb], %rs      6. ld.b %rd, [%rb]      7. ld.h %rd, [%rb]      8. ld.w %rd, [%rb]     ○分岐元      1. 高速RAM      2. SRAM     ○分岐先      1. 高速RAM      2. SRAM     ○メモリアクセス先      1. 高速RAM      2. SRAM 検証用プログラムはこちら:     「call.d %rb」 http://www.piece-me.org/archive/delyerr2-20031222.zip     「ret.d」   http://www.piece-me.org/archive/delyerr3-20031222.zip 検証用プログラムの結果出力ファイル(生のCSV形式)はこちら:     「call.d %rb」 http://www.piece-me.org/piece-lab/delyerr2-20031222-result.csv     「ret.d」   http://www.piece-me.org/piece-lab/delyerr3-20031222-result.csv 出力結果ファイルを読みやすいように加工してみたのがこちら:     「call.d %rb」 http://www.piece-me.org/piece-lab/delyerr2-20031222-kekka.csv     「ret.d」   http://www.piece-me.org/piece-lab/delyerr3-20031222-kekka.csv 結果、     --------------------------------     全ての組み合わせで、誤動作無し!     -------------------------------- (まだメモリアクセス命令「pushn %rs」「popn %rd」との組み合わせを試していませが、多分大丈夫じゃないかな?と思います。) これは助かりました! 前述の通り、手作業で最適化を行う場合に「call.d %rb」「ret.d」を使うケースは、「jp.d %rb」よりもずっと多いです。 「call.d %rb」と「ret.d」に誤動作条件があったら、かなりヤバイことになっているところでした。     『「call.d %rb」「ret.d」の使用においては、仕様外の誤動作を心配する必要はありません』 さて、残るディレイド分岐命令は「jp.d sign8」「jr.d sign8」「call.d sign8」です。 これらの命令も非常に使用頻度が高いですが、これまでにさんざん使って特に問題が出ていないので、多分大丈夫だと思います。 次回、実験予定です。 (続きます…) * Sat Dec 20 12:00:00 JST 2003 Naoyuki Sawa - ディレイド分岐命令の誤動作#2 (前回からの続き…) 何通りかのパターンで検証を行ってみたところ、ディレイド分岐命令が誤動作する条件がおぼろげに見えてきました。 その条件とは、おおよそ次のようなものです。     『ディレイド分岐命令の直前にメモリアクセス命令があると、ディレイド命令が実行されないことがある』 前回はインターロック条件について詳しく触れましたが、どうやらインターロックの有無は関係ない感じです。 例えば、次のようなコードを高速RAMに置いて実行した場合にも誤動作となり、ディレイド命令が実行されません。     ; あらかじめ%r10に分岐先アドレスが入っているものとします。     xld.w %r4, 0x100000     xld.w %r5, [%r4+100] ; 直前にメモリアクセス命令があると…     jp.d %r10       ; インターロックしていなくても…     add %r6, 1      ; ★実行されない!!★ 予想以上に、誤動作の影響を受ける範囲が広いような気がしてきました。 これは詳細に検証してみる必要がありそうです。 それでは、どれぐらいのパターンを検証する必要があるのでしょうか? 少なく見積もっても、次に挙げるぐらいの検証は必要そうです。     ○ディレイド分岐命令      1. jp.d %rb      2. call.d %rb      3. ret.d      4. jp.d sign8      5. jr.d sign8      6. call.d sign8     ○メモリアクセス命令      1. ld.b [%rb], %rs      2. ld.ub [%rb], %rs      3. ld.h [%rb], %rs      4. ld.uh [%rb], %rs      5. ld.w [%rb], %rs      6. ld.b %rd, [%rb]      7. ld.h %rd, [%rb]      8. ld.w %rd, [%rb]      9. pushn %rs     10. popn %rd     ○分岐元      1. 高速RAM      2. SRAM     ○分岐先      1. 高速RAM      2. SRAM     ○メモリアクセス先      1. 高速RAM      2. SRAM 6×10×2×2×2=480通り…ちょっと気が遠くなってきました(^^; さらに欲を言えば、     ・各命令がext命令を伴っているか否か     ・分岐先の最初にある命令の種類によって違いはあるか 等の条件も変えて試してみたいところです。 でも、いきなり全部は無理そうなので、とりあえず今回は次の組み合わせだけ検証してみることにしました。     ○ディレイド分岐命令      1. jp.d %rb     ○メモリアクセス命令      1. ld.b [%rb], %rs      2. ld.ub [%rb], %rs      3. ld.h [%rb], %rs      4. ld.uh [%rb], %rs      5. ld.w [%rb], %rs      6. ld.b %rd, [%rb]      7. ld.h %rd, [%rb]      8. ld.w %rd, [%rb]     ○分岐元      1. 高速RAM      2. SRAM     ○分岐先      1. 高速RAM      2. SRAM     ○メモリアクセス先      1. 高速RAM      2. SRAM 1×8×2×2×2=64通り。 これだけでも、ある程度の傾向はつかめるのではないかと思います。 検証用プログラムはこちら:     http://www.piece-me.org/archive/delyerr1-20031220.zip 検証用プログラムの結果出力ファイル(生のCSV形式)はこちら:     http://www.piece-me.org/piece-lab/delyerr1-20031220-result.csv 出力結果ファイルを読みやすいように加工してみたのがこちら:     http://www.piece-me.org/piece-lab/delyerr1-20031220-kekka.csv Microsoft Excel等のスプレッドシートソフトで読んでください。 出力結果から、いくつかのルールが見て取れます。 ---------------------------- 分岐先メモリの種類は関係ない ---------------------------- 分岐先メモリの種類(高速RAM/SRAM)だけが異なり、他の条件が全て同じケースでは、必ず同じ結果となっています。 分岐先メモリが高速RAMの場合に正常動作なら、分岐先メモリをSRAMに変えてみても正常動作。 分岐先メモリが高速RAMの場合に誤動作なら、分岐先メモリをSRAMに変えてみても誤動作。 このルールから推測すると、たぶん分岐先の最初にある命令の種類も関係ないんじゃないかな?と思います。 -------------------------------- メモリアクセスのサイズは関係ない -------------------------------- メモリアクセスのサイズ(BYTE/UNSIGNED BYTE/HALF/UNSIGNED HALF/WORD)だけが異なり、他の条件が全て同じケースでは、必ず同じ結果となっています。 SRAMへのメモリアクセスの場合、メモリアクセスのサイズによってタイミングが異なるのですが、それは誤動作の条件には関係ないようです。 ----------------------------------------- 分岐元メモリが高速RAMなら、必ず誤動作する ----------------------------------------- 今回の最初に提示した、誤動作するコードの条件がこれに相当します。 高速RAMに転送して使うプログラムを最適化する場合は、ディレイド分岐命令の直前にメモリアクセス命令を置かないよう、注意しなければいけません。 ---------------------------------------------------------------- 書き込みアクセス先メモリと分岐元メモリが共にSRAMなら、誤動作する ---------------------------------------------------------------- 今回の検証で新たに見つけた誤動作条件です。 例えば、次のようなコードをSRAM上で実行すると誤動作し、ディレイド命令が実行されません。     ; あらかじめ%r10に分岐先アドレスが入っているものとします。     xld.w %r4, 0x130000  ; アクセス先メモリ=SRAM     xld.w [%r4], %r5   ; メモリアクセス=書き込み     jp.d %r10     add %r6, 1      ; ★実行されない!!★ なお、このコードを高速RAM上で実行した場合は前出の条件に一致し、やはり誤動作します。 つまり、SRAMへの書き込みの直後にディレイド分岐命令を置いた場合は、高速RAMに転送して実行するプログラムでなくても誤動作する、ということです。 今回の検証のまとめです。 出力結果を見ると、ディレイド分岐命令の直前にメモリアクセス命令を置いても誤動作しないケースはいくつか存在します。 例えば、分岐元プログラムがSRAM上にあり、かつ、直前のメモリアクセスが読み込みならば、誤動作しません。 しかしながら、最後に挙げたような特殊条件下で誤動作するケースがあるため、安全のためには次に示す単純なルールに従っておくのが良さそうです。     『ディレイド分岐命令の直前にはメモリアクセス命令を置かない』 さて、今回はディレイド分岐命令「jp.d %d」についてだけ実験しましたが、他のディレイド分岐命令「call.d %rb」や「jr.d sign8」等ではどうでしょうか? これまでの経験から推測すると、「jr.d sign8」は直前にメモリアクセス命令があっても大丈夫そうな気がするのですが・・・ 次回は、ディレイド分岐命令の種類を変えて試してみようと思います。 (続きます…) * Wed Dec 17 12:55:00 JST 2003 Naoyuki Sawa - ディレイド分岐命令の誤動作#1 「タンクバタリアン/EMU」にて、6502エミュレーション高速化のために手作業でアセンブラプログラムの最適化を行ったのですが、実はかなりハマリました。 問題の個所は比較的小さなサブルーチンで、何度見直しても間違っていないはずなのに、どうやっても期待通りの動作をしないのです。 使用頻度の高いサブルーチンだったので高速RAMに転送して実行していたところを、試しにSRAMに戻してみるとなぜか問題なく実行できてしまう・・・何故? 散々悩んだ末に、どうやら原因らしきものが掴めました。S1C33209 CPUコアの★誤動作★、さもなくばCPUマニュアルの★記載漏れ★っぽいです。 今回は、この件について記録しておこうと思います。 P/ECEのCPU S1C33209は、多くの一般的なRISCプロセッサと同じく、「ディレイド分岐機能」を備えています。 ディレイド分岐機能については、以前の研究記録『実はディレイド命令』でも触れましたので、そちらも併せてご参照ください。 今回問題となった個所は、次のような処理を行う部分でした。(※かなり簡略化しています)     『6502機械語命令を1バイト読み込み、その内容によって分岐する』 C言語で書くと、こんな感じの処理です。 --------------------------------------------------------------------------------------------------     unsigned char* pc = ...;     switch(*pc++) {     case 0: ...; break;     case 1: ...; break;         ...     case 9: ...; break;     } -------------------------------------------------------------------------------------------------- これをアセンブラで書くと、次のようになります。 -------------------------------------------------------------------------------------------------- 1|     ; 2|     ; %r0レジスタに変数pcが割り当てられているものとします。 3|     ; 4|     ld.ub %r1, [%r0]    ; code = *pc 5|     xld.w %r2, TABLE    ; addr = TABLE[code] 6|     sll %r1, 2 7|     add %r1, %r2 8|     ld.w %r3, [%r1] 9|     jp.d %r3        ; goto addr 10|     add %r0, 1       ; pc++ 11|      12| TABLE:             ; switch用ジャンプテーブル 13|     .word CASE0 14|     .word CASE1 15|     ... 16|     .word CASE9 17|     18| CASE0: 19|     ...           ; code=0の場合の処理 20| CASE1: 21|     ...           ; code=1の場合の処理 22|     ... 23| CASE9: 24|     ...           ; code=9の場合の処理 -------------------------------------------------------------------------------------------------- ※これ以降の説明で個々の命令について実行サイクル数を調べて行きますが、とりあえずメモリアクセス時のウェイトサイクルは除外して考えます。 9〜10行目にご注目ください。 ディレイド分岐機能を使って、ジャンプ命令を1サイクル高速化しています。     jp.d %r3    ; 1サイクル     add %r0, 1   ; 1サイクル もしディレイド分岐機能を使わずに普通に書けば、次のようになります。     add %r0, 1   ; 1サイクル     jp %r3     ; 2サイクル 従って、ディレイド分岐機能を使った方が1サイクル速くなります。 ・・・と言いたいところですが、実はこの場合に限って言えば、ディレイド分岐機能を使った場合と使わない場合のサイクル数は変わらないのです。 8〜9行目にご注目ください。     08|    ld.w %r3, [%r1]     09|    jp.d %r3 8行目でメモリから読み込んだ%r3の値を、直後に9行目のジャンプ命令で飛び先アドレスとして参照しています。 S1C33209 CPUではこのような処理を行った場合に「インターロック」と呼ばれる遅延が発生し、命令の実行時間が1サイクル長くなるのです。 詳しくは「S1C33000コアCPUマニュアル(33000Core-J.pdf)」34ページ『3.2 プログラム実行状態 (7) インターロックによる遅延』をご参照ください。 結局、9〜10行目の本当の実行サイクル数は次のようになります。     jp.d %r3    ; 1サイクル+1サイクル(インターロック)     add %r0, 1   ; 1サイクル ディレイド分岐命令を使わない場合は、ジャンプ命令の実行時間が1サイクル長い代わりにインターロックは発生せず、 ディレイド分岐命令を使った場合は、ジャンプ命令の実行時間が1サイクル短い代わりにインターロックが発生します。 従ってこの場合に限って言えば、どちらの方法を使っても全体のサイクル数は3サイクルとなり、実行速度は同じです。 ではなぜディレイド分岐命令を使う方のプログラムを採用したかと言うと、次のような判断によります。 ディレイド分岐命令を使わない方の合計実行サイクル数は、必ず3サイクルとなります。例外はありません。 それに対し、ディレイド分岐命令を使った場合の合計実行サイクル数は、非常に幸運な場合に2サイクルとなる可能性があります。 どんな幸運かと言いますと、8〜9行目の間に割り込みが発生した場合です。 8行目の命令実行直後にタイマやサウンド等の割り込みが発生すると、8行目と9行目の間で割り込み処理を行うことになります。 すると、8行目でメモリから読み込んだ%r3の値を、「直後に」9行目で参照するわけではなくなります。 従って9行目でインターロックは発生せず、9行目と10行目の合計実行サイクル数は2サイクルとなります。 ・・・とは言えこんな幸運は滅多にあるわけでなし、全体の速度から見れば 0.00...01% 程度の違いでしかないでしょう。 どちらの方法を使うかは全く“気分の問題”であり、重要なのは、どちらの方法でも正しく動く、ということです。 ■■■■■■■■■■ ここまでは問題ありません。仕様通りの動作です。問題はここから ■■■■■■■■■■ ところが実は、ある条件が重なった場合に、ディレイド分岐命令を使う方のプログラムは正しく動かなくなります。 ある条件とは、     このサブルーチンを高速RAM上で実行した場合 です。わざわざ手作業で最適化するからには高速性が必要なサブルーチンであり、もちろん高速RAMに転送して実行するでしょう。 今回が正にこのケースに相当しました。 ディレイド分岐命令を使う方のプログラムを高速RAMに置いて実行すると、次のような振る舞いとなります。(※サイクル数は推測です)      8|    ld.w %r3, [%r1]   ; 1サイクル      9|    jp.d %r3       ; 1サイクル+1サイクル(インターロック)      |               ; ★ここでジャンプしてしまう!!★     10|    add %r0, 1      ; ★実行されません!!★ すなわち、ディレイド命令であるはずの10行目「add %r0, 1」が実行されないのです。 C言語に例えると、     switch(*pc++) {         ...     } と書いたつもりが実際の動作は     switch(*pc) {         ...     } になってしまう、という事態。正しい結果が得られるはずもありません。 これは危険です!! もう少し詳しく、症状が発現する条件を調べておく必要がありそうです。 (続きます…) * Mon May 5 05:30:00 JST 2003 Naoyuki Sawa - 警告の範囲が違う 昨日書いた「距離を求める(誤差±約1%・30ビット版)」のアセンブラプログラム、実はかなりハマリました。 どうやっても結果が合わず、さんざん悩んだ末に見つけた原因は次のようなものでした。 正しくは     xld.w %r13, 55 と書くべきところを     ld.w %r13, 55 と書いてしまっていたのです。 P/ECEのCPU S1C33の命令セットは、たいていの場合、即値に6ビットまでの数値しか指定できません。 それ以上のビット数の即値を使いたい場合は、ext命令を使って桁を拡張する必要があります。 上の例に挙げた「ld」命令の場合、即値には符号付き6ビットまでしか指定できません。 符号付き6ビットで表現できる数値は-32〜31ですので、     ・     ・     ・     ld.w %r13, 34      ; 間違い!     ld.w %r13, 33      ; 間違い!     ld.w %r13, 32      ; 間違い!     ld.w %r13, 31      ; 正しい     ld.w %r13, 30      ; 正しい     ld.w %r13, 29      ; 正しい     ・     ・     ・     ld.w %r13, 1      ; 正しい     ld.w %r13, 0      ; 正しい     ld.w %r13, -1      ; 正しい     ・     ・     ・     ld.w %r13, -30     ; 正しい     ld.w %r13, -31     ; 正しい     ld.w %r13, -32     ; 正しい     ld.w %r13, -33     ; 間違い!     ld.w %r13, -34     ; 間違い!     ld.w %r13, -35     ; 間違い!     ・     ・     ・ となります。 また別の例として、「add」命令などは即値に符号なし6ビットまでの数値しか指定できません。 符号なし6ビットで表現できる数値は0〜63ですので、     ・     ・     ・     add %r13, 66      ; 間違い!     add %r13, 65      ; 間違い!     add %r13, 64      ; 間違い!     add %r13, 63      ; 正しい     add %r13, 62      ; 正しい     add %r13, 61      ; 正しい     ・     ・     ・     add %r13, 1       ; 正しい     add %r13, 0       ; 正しい     add %r13, -1      ; 間違い!     add %r13, -2      ; 間違い!     add %r13, -3      ; 間違い!     ・     ・     ・ となります。 さて、今回のミス     ld.w %r13, 55 は、「ld」命令が扱える符号付き6ビットの範囲(-32〜31)を外れた即値(55)を指定してしまったことです。 確かに当方のミスなのですが、本来、こういうミスに対してはアセンブラが警告を発することになっています。     「Warning: Numeric range.:インストラクションオペランドの数値が不正です。」     S1C33 Family Cコンパイラパッケージマニュアル Ver.4 (S5U1C33000C_J.pdf) 179ページより ところが、アセンブラは上のコードに対して警告を発しませんでした。 そして、55を無理やり符号付き6ビットと解釈した値で、命令コードを生成してしまっていたのです。     ld.w %r13, -9      ; -9 … 55を無理やり符号付き6ビットと解釈した値 なぜ警告が発せられなかったのでしょうか? いくつか試してみたところ、S1C33 CPUのアセンブラas33.exeは、警告とする数値の範囲を間違えているようです。 「ld」命令といろいろな数値を組み合わせて、警告が発せられるかどうかを試してみました。     ・     ・     ・     ld.w %r13, 66      ; 間違い!   ; 警告あり     ld.w %r13, 65      ; 間違い!   ; 警告あり     ld.w %r13, 64      ; 間違い!   ; 警告あり     ld.w %r13, 63      ; 間違い!   ; 警告なし!危険!     ld.w %r13, 62      ; 間違い!   ; 警告なし!危険!     ld.w %r13, 61      ; 間違い!   ; 警告なし!危険!     ・     ・     ・     ld.w %r13, 34      ; 間違い!   ; 警告なし!危険!     ld.w %r13, 33      ; 間違い!   ; 警告なし!危険!     ld.w %r13, 32      ; 間違い!   ; 警告なし!危険!     ld.w %r13, 31      ; 正しい    ; 警告なし     ld.w %r13, 30      ; 正しい    ; 警告なし     ld.w %r13, 29      ; 正しい    ; 警告なし     ・     ・     ・     ld.w %r13, 1      ; 正しい    ; 警告なし     ld.w %r13, 0      ; 正しい    ; 警告なし     ld.w %r13, -1      ; 正しい    ; 警告なし     ・     ・     ・     ld.w %r13, -30     ; 正しい    ; 警告なし     ld.w %r13, -31     ; 正しい    ; 警告なし     ld.w %r13, -32     ; 正しい    ; 警告なし     ld.w %r13, -33     ; 間違い!   ; 警告なし     ld.w %r13, -34     ; 間違い!   ; 警告なし     ld.w %r13, -35     ; 間違い!   ; 警告なし     ・     ・     ・ 32〜63までの数値が、正しい命令コードが生成されないにも関わらず無警告となっています。 「add」命令についても同様に試してみます。     ・     ・     ・     add %r13, 66      ; 間違い!   ; 警告あり     add %r13, 65      ; 間違い!   ; 警告あり     add %r13, 64      ; 間違い!   ; 警告あり     add %r13, 63      ; 正しい    ; 警告なし     add %r13, 62      ; 正しい    ; 警告なし     add %r13, 61      ; 正しい    ; 警告なし     ・     ・     ・     add %r13, 1       ; 正しい    ; 警告なし     add %r13, 0       ; 正しい    ; 警告なし     add %r13, -1      ; 間違い!   ; 警告なし!危険!     add %r13, -2      ; 間違い!   ; 警告なし!危険!     add %r13, -3      ; 間違い!   ; 警告なし!危険!     ・     ・     ・     add %r13, -30      ; 間違い!   ; 警告なし!危険!     add %r13, -31      ; 間違い!   ; 警告なし!危険!     add %r13, -32      ; 間違い!   ; 警告なし!危険!     add %r13, -33      ; 間違い!   ; 警告あり     add %r13, -34      ; 間違い!   ; 警告あり     add %r13, -35      ; 間違い!   ; 警告あり     ・     ・     ・ -32〜-1までの数値が、正しい命令コードが生成されないにも関わらず無警告となっています。 どうやらas33.exeは、命令によって有効な数値の範囲が違うことを忘れ、常に-32〜63が正しい数値の範囲だと思ってしまっているようです。 今回は小さなプログラムだったので間違いに気付いたものの、大きなプログラムで間違ったコードが無警告で通ってしまっていたら・・・ この危険を回避するには、「ld」や「add」等の命令で即値を使う場合は、たとえ小さな値でも常に「xld」「xand」等と表記しましょう。 各命令の有効な数値範囲を超えると、コンパイラが自動的に「ext」命令を付加して、誤った命令コードの生成を防いでくれます。 * Sun May 4 15:00:00 JST 2003 Naoyuki Sawa - 距離を求める(誤差±約1%・30ビット版) P/wareさん(http://www.aw.wakwak.com/~hitode/piece/)のページの、 距離を求める(http://www.aw.wakwak.com/~hitode/piece/index.html#dist2)高速な方法が更新されています。 まず、前回と同じ誤差±約4%の方法で、より精度の高い係数が公開されました。 前回のプログラムを、新しい係数で作り直したものがこちら。     http://www.piece-me.org/archive/hypot2a-20030504.zip さらに、「誤差約1%」と「誤差約0.2%」の計算方法が追加されています。 誤差約1%の方法が、速度と精度のバランスが取れていて、良さそうです。 そこで今回は、誤差約1%の計算方法の30ビット版を作ってみました。     /* 原点と点 (x, y) の距離を求めます。      * [in]      *   x, y      距離を求める座標      * [out]      *   戻り値     原点と点 (x, y) の距離      * [note]      *   * 距離の近似をもとめる計算式:      *       |x|>|y|のとき、|x|+((|y|*|y|/|x|*55)>>7)      *       (誤差は±約1%)      */     int hypot4(int x, int y);         asm("         .code         .align 1         .global hypot4     hypot4:         ld.w %r10, %r12     ; if(x == 0 && y == 0) return 0         or %r10, %r13         jreq hypot4_exit         ;         cmp %r12, 0       ; %r12 = |x|         jrge 3          not %r12, %r12          add %r12, 1         cmp %r13, 0       ; %r13 = |y|         jrge 3          not %r13, %r13          add %r13, 1         cmp %r12, %r13      ; if(|x| < |y|) %r12 exch %r13         jrge 4          xor %r13, %r12          xor %r12, %r13          xor %r13, %r12         ;         mltu.w %r13, %r13    ; %r5:%r4 = |y|^2         ld.w %r4, %alr         ld.w %r5, %ahr         ;         xld.w %r13, 55      ; %r6:%r5:%r4 = |y|^2 * 55         mltu.w %r4, %r13         ld.w %r4, %alr         ld.w %r6, %ahr         mltu.w %r5, %r13         ld.w %r5, %alr         add %r5, %r6         ;{{         jruge.d 3        ; (!C)          ld.w %r6, %ahr     ; (undoc'd delay)          add %r6, 1         ;}}または{{         ;ld.w %r6, %ahr         ;adc %r6, %r8      ; -gp=0x0 を想定         ;}}         ;         ld.w %r13, %r5      ; %r5:%r4 = (|y|^2 * 55) >> 7         xsll %r13, 25         xsrl %r4, 7         or %r4, %r13         xsll %r6, 25         xsrl %r5, 7         or %r5, %r6         ;         ld.w %alr, %r4      ; %alr = ((|y|^2 * 55) >> 7) / |x|         div0u %r12         ld.w %ahr, %r5         div1 %r12        ; 1         div1 %r12        ; 2         div1 %r12        ; 3         div1 %r12        ; 4         div1 %r12        ; 5         div1 %r12        ; 6         div1 %r12        ; 7         div1 %r12        ; 8         div1 %r12        ; 9         div1 %r12        ; 10         div1 %r12        ; 11         div1 %r12        ; 12         div1 %r12        ; 13         div1 %r12        ; 14         div1 %r12        ; 15         div1 %r12        ; 16         div1 %r12        ; 17         div1 %r12        ; 18         div1 %r12        ; 19         div1 %r12        ; 20         div1 %r12        ; 21         div1 %r12        ; 22         div1 %r12        ; 23         div1 %r12        ; 24         div1 %r12        ; 25         div1 %r12        ; 26         div1 %r12        ; 27         div1 %r12        ; 28         div1 %r12        ; 29         div1 %r12        ; 30         div1 %r12        ; 31         div1 %r12        ; 32         ;         ld.w %r10, %alr     ; %r10 = ((|y|^2 * 55) >> 7) / |x| + |x|         add %r10, %r12     hypot4_exit:         ret         "); オリジナルの計算式は     |x|+((|y|*|y|/|x|*55)>>7) なのですが、|y|*|y| と |x| の値が近いときに誤差が大きくなり、例えば (-13, 8) で約7%の誤差が発生してしまいます。 そこで、ちょっと計算式をアレンジして、     |x|+((|y|*|y|*55>>7)/|x|) としてみました。 この関数を含む、動作検証用プログラムはこちら。     http://www.piece-me.org/archive/hypot4-20030504.zip 前回と同じく、ランダムな座標 (x, y) と原点 (0, 0) 間の距離を求め、本当の値との誤差を表示します。 10万回程度繰り返してみたところ、ほとんど±1%以内に収まり、最悪でも1.5%を超えることはありませんでした。 前回の4%・30ビット版は、一回あたり約30サイクル前後で完了していました。 今回の1%・30ビット版は、一回あたり約90サイクル前後かかってしまいます。 (いずれもソースコード上での合計値で、実測値ではありません。メモリウェイト等も考慮していません。) しかしながら、三倍程度の遅さで四倍の精度が得られるのなら、1%版の方を使おうかな?と思いました。 * Sat May 3 08:50:00 JST 2003 Naoyuki Sawa - 距離を求める(30ビット版) P/wareさん(http://www.aw.wakwak.com/~hitode/piece/)のページで、 距離を求める(http://www.aw.wakwak.com/~hitode/piece/index.html#dist)高速な方法が説明されています。 特に、誤差4%の近似式を使った方法 hypot2() が参考になります。 僕はこれまでマトモに平方根を計算してました・・・これからはこの方法を使わせて頂きます。 先のページにも説明されている通り、hypot2() は (x, y) が 16ビットまでの数値でないと正しく動きません。 固定小数点数などを使っていると、16ビット以上の座標値や距離が必要になってくることも結構あります。 そこで、誤差を増やさずに、30ビットまでの (x, y) に対応した版を作ってみました。 なぜ30ビットまでなのか(31ビットや32ビットの座標値には対応できないのか)というと、 31ビット以上の座標値では結果の距離そのものが32ビット符号付き整数をオーバーフローしてしまうからです。     /* 原点と点 (x, y) の距離を求めます。      * [in]      *  x, y      距離を求める座標      * [out]      *  戻り値     原点と点 (x, y) の距離      * [note]      *  * 距離の近似をもとめる計算式:      *   |x|>|y|のとき、|x|*0.9604+|y|*0.3978      *   (誤差は±約4%)      */     int hypot2(int x, int y);         asm("         .align 1         .global hypot2     hypot2:         cmp %r12, 0        ; %r12 = |x|         jrge 3          not %r12, %r12          add %r12, 1         cmp %r13, 0        ; %r13 = |y|         jrge 3          not %r13, %r13          add %r13, 1         cmp %r12, %r13      ; if(|x| < |y|) %r12 exch %r13         jrge 4          xor %r13, %r12          xor %r12, %r13          xor %r13, %r12         xld.w %r4, 0xf5dcc63f   ; %r10 = |x| * (0.9604 << 32)         mltu.w %r12, %r4         ld.w %r10, %ahr         xld.w %r4, 0x65d63886   ; %r10 += |y| * (0.3978 << 32)         mltu.w %r13, %r4         ld.w %r4, %ahr         add %r10, %r4         ret         "); 使い方はオリジナルの hypot2() と同じです。 この関数を含む、動作検証用プログラムはこちら。     http://www.piece-me.org/archive/hypot-20030503.zip ランダムな座標 (x, y) と原点 (0, 0) 間の距離を求め、本当の値との誤差を表示します。 10000回程度繰り返してみたところ、確かに誤差が±4%を超えることはありませんでした。 余談ながら、インラインアセンブラ asm("〜") を関数定義の外側で使っている点にご注目ください。 Cコンパイラが暗黙に生成するコードの心配をしなくていいので、僕は最近この方法を好んで使ってます。 (単独のアセンブラソースに分けるのが面倒なので・・・(^^;) * Fri Apr 12 20:30:00 JST 2003 Naoyuki Sawa - 実はディレイド命令 P/ECEのCPU S1C33209は、RISC CPUの特徴のひとつ、「ディレイド分岐機能」を持っています。 ディレイド分岐機能とは、分岐命令の実行より先に分岐命令の直後の命令を実行し、実行効率を稼ぐ機能です。 ディレイド分岐機能を使う分岐命令を、「ディレイド分岐命令」と呼びます。 また、ディレイド分岐命令の直後に置かれる命令を、「ディレイド命令」と呼びます。 ほとんどの分岐命令は、ディレイド分岐命令として利用可能です。 しかし、ディレイド命令として利用可能な命令はある程度限られています。 単純なレジスタ間ロード命令や加減算・論理演算命令は、だいたいディレイド命令として利用可能なのですが、 直感に反してディレイド不可な命令もいくつかあるので注意が必要です。 例えば、符号拡張・ゼロ拡張を伴うレジスタ間ロード命令がディレイド不可というのは、ちょっと戸惑います。     ld.b %rd, %rs     ×ディレイド不可     ld.ub %rd, %rs    ×ディレイド不可     ld.h %rd, %rs     ×ディレイド不可     ld.uh %rd, %rs    ×ディレイド不可     ld.w %rd, %rs     ○ディレイド可能 その他に、汎用レジスタと特殊レジスタ間のロード命令もディレイド不可となっています。     ld.w %rd, %ss     ×ディレイド不可     ld.w %sd, %rs     ×ディレイド不可 汎用レジスタと特殊レジスタ間のロード命令がディレイド不可というのは、感覚的に理解できなくもありません。 ・・・と思っていたのですが、実はディレイド可能でした。 今回は、この件について記録しておこうと思います。 S1C33000コアCPUマニュアルによると、「ld.w %rd,%ss」「ld.w %sd,%rs」はディレイド不可となっています。 (33000Core-J.pdf p.112「ld.w %rd,%ss」, p.117「ld.w %sd,%rs」, Appendix-1「Quick Reference」参照) しかし実際には、これらの命令もディレイド命令として利用できるようです。 なぜなら、EPSON社純正ライブラリの除算ルーチンが、これらをディレイド命令として使っているからです。 例えば、符号付き除算ルーチンは次のように実装されています。     __divsi3:         ld.w %alr, %r12         div0s %r13         ld.w %r4, 0x4         ld.w %r5, %psr     __divsi3_loop_start:         div1 %r13         div1 %r13         div1 %r13         div1 %r13         div1 %r13         div1 %r13         div1 %r13         div1 %r13         sub %r4, 0x1         jrne.d __divsi3_loop_start   <= 次の命令がディレイド命令になります         ld.w %psr, %r5         <= ld.w %sd(0),%rs(5) に相当します         div2s %r13         div3         ret.d              <= 次の命令がディレイド命令になります         ld.w %r10, %alr         <= ld.w %rd(10),%ss(2) に相当します S1C33000コアCPUマニュアルによると、規定のディレイド可能な命令以外をディレイド命令として用いた場合、 「動作が不安定となる」(p.30「ディレイド分岐機能」参照)とあります。 前述の除算ルーチンのケースはたまたま動いていて、別の命令並びでは「動作が不安定となる」可能性もあるのですが、 わざわざそんな危険な実装をするとは考えづらいので、たぶんいつでもディレイド可能なんじゃないかな、と思います。 * Fri Feb 14 12:30:00 JST 2003 Naoyuki Sawa - memcpy()でアドレス不整例外 『XMプレイヤー』作成において、なかなか消えない「アドレス不整例外」に悩まされました。 今回は、その顛末をまとめておこうと思います。 P/ECEのCPU・S1C33209は、「不整アドレス」からのデータ読み出しを行おうとすると、「アドレス不整例外」を発生します。 「不整アドレス」とは、読み出そうとしているデータ型のサイズの倍数になっていないアドレスのことです。 例えば、int型データを読み出す場合、intは4バイトですので、読み出すアドレスも4の倍数でなければいけません。 同様に、short型データを読み出す場合は、shortが2バイトですから、読み出すアドレスも2の倍数でなければいけません。 「アドレス不整例外」が発生すると、画面には「Address error exception」と表示され、リセットを押すしかなくなります。 以下に、「アドレス不整例外」を発生するコード例を、いくつか示します。     int *p, v; p = (int*)0x120000; v = *p;  /* 0x120000番地は4の倍数なので、「アドレス不整例外」は発生しません */     int *p, v; p = (int*)0x120001; v = *p;  /* 「アドレス不整例外」が発生します! */     int *p, v; p = (int*)0x120002; v = *p;  /* 「アドレス不整例外」が発生します! */     int *p, v; p = (int*)0x120003; v = *p;  /* 「アドレス不整例外」が発生します! */     int *p, v; p = (int*)0x120004; v = *p;  /* 0x120004番地は4の倍数なので、「アドレス不整例外」は発生しません */     shrot *p, v; p = (shrot*)0x120000; v = *p;  /* 0x120000番地は2の倍数なので、「アドレス不整例外」は発生しません */     shrot *p, v; p = (shrot*)0x120001; v = *p;  /* 「アドレス不整例外」が発生します! */     shrot *p, v; p = (shrot*)0x120002; v = *p;  /* 0x120002番地は2の倍数なので、「アドレス不整例外」は発生しません */ 通常のプログラムでは、「アドレス不整例外」に注意する必要はありません。 intデータは4の倍数アドレスに、shortデータは2の倍数アドレスに、コンパイラが適切に配置してくれるからです。 注意が必要になるのは、あらかじめ決められた形式を持つデータ構造から、データを読み出す場合です。 今回問題となったXM形式のデータ構造がまさにそれです。 XM形式のデータ構造では、奇数個のchar型データの直後に、空白なしでint型データが配置されていたりします。 つまり、int型のデータが「不整アドレス」に配置されていることになります。 メモリ上に読み込んだXM形式のデータ構造から、このようなint型データを読み出そうとすると、「アドレス不整例外」が発生します。 =============================================================================================================================== 「不整アドレス」からの読み出しができない、という制約を持つのは、S1C33209だけが特別ではありません。 いちばんメジャーなPentiumシリーズCPUにはこの制約がないものの、他の多くのCPUは同様の制約を持っています。 ですから、「不整アドレス」にデータが配置されている可能性のあるようなデータ構造からデータ読み出しを行う場合は、 「アドレス不整例外」が発生しないよう、プログラム側で対処しなければいけません。 いちばんオーソドックスなのは、次のような方法です。 どんなアドレスからでも「アドレス不整例外」なしにint型データを読み出せるような、補助関数を用意します。     int read_int(const int* p) {         const char* pp = (const char*)p;         return (pp[0] << 24) | (pp[1] << 16) | (pp[2] << 8) | (pp[3]);     } 4バイトの読み出しを1バイトづつ行ってから、int型に組み立てて返します。 1バイトづつの読み出しならば、絶対に「アドレス不整例外」は発生しません。(1の倍数でないアドレスはないから) この補助関数を使えば、引数pにどんなアドレスが指定されたとしても、そこからint型のデータが読み出せるわけです。 それでは、この補助関数を使って、先ほど挙げた例を書き直してみます。     int *p, v; p = (int*)0x120000; v = read_int(p);  /* OK! */     int *p, v; p = (int*)0x120001; v = read_int(p);  /* OK! */     int *p, v; p = (int*)0x120002; v = read_int(p);  /* OK! */     int *p, v; p = (int*)0x120003; v = read_int(p);  /* OK! */     int *p, v; p = (int*)0x120004; v = read_int(p);  /* OK! */ =============================================================================================================================== 以上の方法が、オーソドックスかつ正しい方法です。 ・・・が、しかし。僕はズボラなので、read_int()を次のように定義してしまいます。     int read_int(const int* p) { int v; memcpy(&v, p, sizeof v); return v; } どこらへんがズボラかといいますと、いろいろな型のための補助関数を追加する場合、機械的な書き換えで対応できる点です。      int read_int (const int * p) { int v; memcpy(&v, p, sizeof v); return v; }      short read_short (const short * p) { short v; memcpy(&v, p, sizeof v); return v; }     unsigned int read_unsigned_int (const unsigned int * p) { unsigned int v; memcpy(&v, p, sizeof v); return v; }     unsigned short read_unsigned_short(const unsigned short * p) { unsigned short v; memcpy(&v, p, sizeof v); return v; }      float read_float (const float * p) { float v; memcpy(&v, p, sizeof v); return v; }      double read_double (const double* p) { double v; memcpy(&v, p, sizeof v); return v; } ちなみに、オーソドックスな方法でread_short()やread_double()を実装すると、こうなります。     short read_short(const short* p) {         const char* pp = (const char*)p;         return (pp[0] << 8) | (pp[3]);     }     float read_double(const double* p) {         const char* pp = (const char*)p;         char v[8];         v[0] = pp[0];         v[1] = pp[1];         v[2] = pp[2];         v[3] = pp[3];         v[4] = pp[4];         v[5] = pp[5];         v[6] = pp[6];         v[7] = pp[7];         return *(double*)v;     } まあこの程度ですので大した手間ではないのですが、この程度の手間も惜しむほどズボラだという…(^^; これまでP/ECE以外の環境でも、memcpy()を使ったズボラ版補助関数を使っていましたし、それで上手くいっていると思っていました。 ・・・が、それは間違いでした。 =============================================================================================================================== ズボラ版補助関数は、memcpy()が内部でデータを1バイトづつコピーしてくれることを前提としています。 いや、良く出来たmemcpy()の実装の場合、指定された転送元・転送先アドレスと転送サイズによっては、 数バイトづつのコピーを行って処理を高速化するとは聞いていましたが、 少なくとも「アドレス不整例外」が発生するような方法は採られないものと思い込んでいました。 ところが、コンパイラによる最適化が絡んでくると、必ずしもそうとは言えないようなのです。 それではまず、「アドレス不整例外」を発生するmemcpy()の使用例を示します。 -------------------------------------------------------------------------------------------------------------------------------     #include     #include          volatile int* ptr;     volatile int val;     volatile char data[] = { 0,1,2,3,4,5 };     /* ここを読みます ~~~~~~~ */          int read_int(const int* p) { int v; memcpy(&v, p, sizeof v); return v; }          void pceAppInit() { }     void pceAppExit() { }     void pceAppProc(int count) {         ptr = (int*)&data[1];         val = read_int(ptr);     } ------------------------------------------------------------------------------------------------------------------------------- 変数にvolatile指定を追加しているのは、最適化によって読み出し処理そのものが削られてしまわないようにするためです。 特に重要な意味はありません。 さて、このプログラムを最適化オプション「-O2」を指定してコンパイルします。 そして実行すると・・・「アドレス不整例外」が発生します! 「-ls」オプションを指定してシンボルファイルを作成してみると、data[]配列は0x100264番地に確保されていました。 プログラムでは&data[1]から読み出そうとしているので、つまり0x100265番地からint型データを読み出していることになります。 そのまま読み出したのでは「アドレス不整例外」が発生するのは必然ですが、今回はちゃんと補助関数を使っています。 にもかかわらず「アドレス不整例外」発生。なぜ? =============================================================================================================================== memcpy()のところで、コンパイラはどのようなコードを生成しているのでしょうか? 「-b」オプションを指定して、コンパイラが生成したアセンブラソースを確認してみます。 ※読みやすいように、少し編集してあります。 ----------------------------------------[S1C33用gccの生成コード]---------------------------------------------------------------     read_int:         xsub %sp, %sp, 4         xld.w %r10, [%r12]         xld.w [%sp], %r10         xadd %sp, %sp, 4         ret          pceAppProc:         xld.w %r12, data+1         xld.w [ptr], %r12         xcall read_int         xld.w [val], %r10         ret ------------------------------------------------------------------------------------------------------------------------------- なんと、単なるint型データの読み出し・格納になってしまっています! 「アドレス不整例外」が発生するわけです。 これは明らかに、コンパイラの最適化による副作用です。 では、このような最適化が行われるのは ・P/ECEのコンパイラ(S1C33用gcc)が特別なのでしょうか? ・それとも、gccならどれでもそうなのでしょうか? ・あるいは、どのコンパイラでもたいていこうなるのでしょうか? まずは、他のCPU用のgccで確認してみます。 SPARC用gccで同様のプログラムを試してみると、やはり「アドレス不整例外」になりました。 (Linux上で試したので、画面表示は「Bus Error」となります。) コンパイラが生成したアセンブラソースは、次のようになります。 ※読みやすいように、少し編集してあります。 ----------------------------------------[SPARC用gccの生成コード]---------------------------------------------------------------     read_int:         retl         ld [%o0], %o0          main:         save %sp, -104, %sp         sethi %hi(data+1), %o0         call read_int,0         or %o0, %lo(data+1), %o0         ret         restore %g0, %o0, %o0 ------------------------------------------------------------------------------------------------------------------------------- 僕は、SPARCプロセッサのアセンブラはぜんぜん読めませんが、read_int()が1バイトづつコピーしていないのはわかります。 (あと余談ですが、ディレイスロットによる最適化で命令の順番が入れ替わったりしているのが見えます。  また、P/ECEのS1C33用gccは、SPARC用gccに較べて、不要なローカル変数の削除による最適化がまだまだ弱いこともわかります。) どうやらgccを使って最適化コンパイルを行うと、だいたい同じような結果になるようです。 それでは、別のコンパイラではどうでしょうか? VisualC++6.0を使って、同様のプログラムをコンパイルしてみます。 最適化オプションは、Releaseビルドの初期設定のままとしました。 ※読みやすいように、少し編集してあります。 ----------------------------------------[Pentium用VisualC++6.0の生成コード]----------------------------------------------------     read_int:         mov eax, dword ptr [esp+4]         mov eax, dword ptr [eax]         ret          main:         push offset data+1         call read_int         add esp, 4         ret ------------------------------------------------------------------------------------------------------------------------------- 同じです。やはり、read_int()は1バイトづつコピーしていません。 (ただしPentiumの場合はそもそも「アドレス不整例外」の制約がありませんので、このプログラムを実行しても問題は出ません。) どうやらどのコンパイラでも、memcpy()が最適化されて不整アドレスからの読み出しが発生する可能性はあるようです。 しかし、こんな重大な問題にこれまで気が付かなかったとは・・・ 例えば、P/ECEで仮想VRAMの(0,1)〜(0,4)のピクセルを(1,1)〜(1,4)へコピーしようとしたら、「アドレス不整例外」になるのでしょうか? -------------------------------------------------------------------------------------------------------------------------------     #include          unsigned char vbuff[128 * 88];          void pceAppInit() {         pceLCDSetBuffer(vbuff);         pceLCDDispStart();     }     void pceAppExit() { }     void pceAppProc(int count) {         unsigned char *src, *dst;         src = &vbuff[ 1];         dst = &vbuff[129];         memcpy(dst, src, 4);         pceLCDTrans();     } ------------------------------------------------------------------------------------------------------------------------------- 試してみると・・・「アドレス不整例外」は発生しません。 そりゃあ 、これで「アドレス不整例外」が発生するのなら、とっくに気付いているはずですよね。 生成されるコードは次のようになります。 ※読みやすいように、少し編集してあります。 -------------------------------------------------------------------------------------------------------------------------------     pceAppProc:         xld.w %r13, vbuff+1         xadd %r12, %r13, 128         xld.w %r14, 0x00000004         xcall memcpy         xcall pceLCDTrans         ret ------------------------------------------------------------------------------------------------------------------------------- 同じ4バイトのコピーなのに、こちらはちゃんとmemcpy()関数が呼び出されています。 「アドレス不整例外」になる例との違いは・・・memcpy()に渡しているポインタの型でしょうか? それでは、プログラムを次のように変更して、もういちど試してみます。 -------------------------------------------------------------------------------------------------------------------------------     #include          unsigned char vbuff[128 * 88];          void pceAppInit() {         pceLCDSetBuffer(vbuff);         pceLCDDispStart();     }     void pceAppExit() { }     void pceAppProc(int count) {         int *src, *dst;         src = (int*)&vbuff[ 1];         dst = (int*)&vbuff[129];         memcpy(dst, src, 4);         pceLCDTrans();     } ------------------------------------------------------------------------------------------------------------------------------- で、実行してみると・・・「アドレス不整例外」が発生しました! 生成されるコードは次の通り。 ※読みやすいように、少し編集してあります。 -------------------------------------------------------------------------------------------------------------------------------     pceAppProc:         xld.w %r11, vbuff+1         xld.w %r10, [%r11]         xld.w [%r11+128], %r10         xcall pceLCDTrans         ret ------------------------------------------------------------------------------------------------------------------------------- vbuff[1]の位置からint型で読み出しを行い、vbuff[1+128]の位置に格納しようとしています。 なんとなく見えてきました。     memcpy()にint*型のポインタを渡すと、単なるint型データの読み出しとして最適化される可能性がある。     その際、ポインタが「不整アドレス」を指しているかどうかは調べられない。     結果、ポインタが「不整アドレス」を指していたなら、「アドレス不整例外」が発生する。 P/ECEの仮想VRAMを扱う場合は、たいていunsigned char*型のポインタを渡すので、問題にならなかったのですね。 =============================================================================================================================== 回避方法は次のとおりです。 1.オーソドックスなread_int()を使う。   つまり、memcpy()を使わず、明示的に1バイトづつ読み出して、組み立てる。 2.ズボラ版read_int()の引数型を変更する。   修正後のread_int()は、次のようになります。     int read_int(const void* p) { int v; memcpy(&v, p, sizeof v); return v; }   『XMプレイヤー』では、この方法で解決しました。 3.最適化しない。   「-O2」オプションを指定しません。しかしこれは、現実的な方法ではありません。   S1C33用gccには「-mno-memcpy」というオプションがあります。   gcc33コマンドの使い方表示には「inline functions 'strcpy' and 'memcpy'」と書かれており、   このコンパイルオプションを使ってmemcpy()の最適化だけを制御できそうに見えます。   が、実際にはこのオプションは、memcpy()の最適化には関係ないようです。     gcc33 -S -O sample.c     gcc33 -S -O -mmemcpy sample.c     gcc33 -S -O -mno-memcpy sample.c   いずれの方法でコンパイルしてもmemcpy()は最適化されてしまい、「アドレス不整例外」が発生します。   そもそもpcc33経由では、gcc33にこのオプションを渡せません。   それに、一部のmemcpy()最適化による問題を回避するためだけに、全部の最適化を抑制してしまうというのはやりすぎです。 =============================================================================================================================== さて、コンパイラにケチを付けるのも身の程知らずですが、しかしこれはちょっと納得いかないような気もします。 memcpy()を最適化してくれるのはありがたいのですけれど、memcpy()のプロトタイプはもともと次のようなものです。     void* memcpy(void* dst, const void* src, size_t len); memcpy()自身は、受け取ったポインタの正確な型を知っていてはいけないはずです。 int*型ポインタだから最適化するとか、char*型ポインタだから最適化しない、というやり方は不可だと思うのですが… C言語仕様的にはどうなのでしょうか? もしかしたら、int*型ポインタに「不整アドレス」を代入した時点で既にC言語仕様違反なのでしょうか? 要調査です。 * 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ってもっと快適なんでしょうか? * Fri Dec 28 21:31:41 JST 2001 Naoyuki Sawa (29日更新分) - 「送る」と「アプリケーションから開く」の違い 今回は、P/ECEではなく、Windowsの話題です。 mini ISDのバグがひとつ取れました。 右クリックして「送る」からの書き込みならちゃんと動くのに、 「アプリケーションから開く」からの書き込みだとエラーになっていた原因。 それは、 ・「送る」の場合はファイル名は"〜"で囲まれないが ・「アプリケーションから開く」だと"〜"で囲まれる というものでした。 例えば、「c:\usr\piece\app\pex\tank.pfs」を書き込む場合、「送る」なら minisd.exe c:\usr\piece\app\pex\tank.pfs と起動されます。これに対し、「アプリケーションから開く」を使った場合は minisd.exe "c:\usr\piece\app\pex\tank.pfs" と起動されます。 拡張子関連付けの場合も、「アプリケーションから開く」の場合と同様です。 これって、こういう仕様なのでしょうか?僕は、知りませんでした。 「アプリケーションから開く」の形式に対応する方法としては、 "〜"で囲まれたファイル名をきちんと解析すべきなのですが、 とりあえず、ファイル名の切り分け文字として、空白文字に加えて ダブルクオーテーションも含めてしまいました^^; * Thu Dec 27 22:39:04 JST 2001 Naoyuki Sawa - 極小フォント正式記載 またまたP/ECEのアップデートです。もはや絶句。恐るべきパワーだ... さて、以前の話題にも取り上げましたpceFontSetType(2)の極小フォントが、 APIリファレンスに記載されていました。これで安心して使えますね。 そして...猪名川で売ろう!がリリースされました。 アクアプラスさん、目玉ゲームなのに、アナウンスが地味すぎです^^; トップページのP/ECEの画面にバーン!と貼り込むぐらいやんないと。 * Thu Dec 27 07:46:43 JST 2001 Naoyuki Sawa - 逐次処理プログラム#2 細胞分裂アオミドロンのバージョン0.01を公開しました。 ぜんぜん未完成でお恥ずかしい限りですが、 実はもっとお恥ずかしいのはソースコードなのです! 以前、逐次処理プログラムのスケルトンをご紹介しましたが、 このゲームはまさにそれを使って書いてあります。 main()から始まってべた〜っと流れる、 P/ECEにあるまじき最悪の設計をご堪能ください。 * Tue Dec 25 05:44:09 JST 2001 Naoyuki Sawa - PCEWAVEINFOはauto変数にしてはいけない なにをいまさら...って感じですが、僕は今日気がついたので^^;書きます。 ADPCM再生を簡単に行うため、次のような関数を用意したとしましょう。 void sound_play(int ch, const void* data) { PCEWAVEINFO winfo; winfo = *(PCEWAVEINFO*)((char*)data + 8); winfo.pData = (char*)data + 8 + sizeof(PCEWAVEINFO); pceWaveDataOut(ch, &winfo); } しかしこのプログラムは、遅かれ早かれ、異常動作を起こします。 その理由は、サウンド再生完了時に、winfo.statにPW_STAT_ENDが 書き込まれるからです。実際には、サウンド再生完了時にはとっくに sound_play()関数からリターンしていて、winfoの実体はありません。 そのアドレスには、別の関数のローカル変数などが存在するでしょう。 つまり、別の変数にいきなりPW_STAT_ENDが書き込まれることになり、 プログラムは異常動作を起こしてしまうのです。 sound_play()関数は、次のように書き換えると正しく動きます。 void sound_play(int ch, const void* data) { static PCEWAVEINFO winfo[4]; winfo[ch] = *(PCEWAVEINFO*)((char*)data + 8); winfo[ch].pData = (char*)data + 8 + sizeof(PCEWAVEINFO); pceWaveDataOut(ch, &winfo[ch]); } 再生チャンネルごとにPCEWAVEINFOを分ける必要はないのですが、気分の問題です。 より正確には、開発キット付属のadpcm/hello.cをご覧下さい。 そう、付属のadpcm/hello.cサンプルコードには、解答が載っていたのです。 にもかかわらず、僕がこの件でハマってしまった原因は、APIリファレンスにあります。 P/ECE APIリファレンスには、次のように記述されています。 > ver1.07 以降は PW_TYPE_CONT を指定しない場合に限り > *pwave は即座に解放されます。 これでは、PCEWAVEINFOはスタック上に取れると思ってしまいますよね?ね? そもそも、再生終了時にPW_STAT_ENDを書き込んでくれなくて結構です。 ちょっとぐちっぽくなってしまいました。ごめんなさい。 メリークリスマス! * Sun Dec 23 07:24:23 JST 2001 Naoyuki Sawa - memset()の引数にご用心 P/ECE付属のコンパイラで、次のようなコードをコンパイルしてみましょう。 memset(vbuf, sizeof vbuf); これ、明らかに memset(vbuf, 0, sizeof vbuf); の書き間違いなのですが、しかしエラーなくコンパイルできてしまいます。 もちろん、正しく動作しません。 なぜ、こうなるか。 memset()はstring.hで宣言されているのですが、 string.hでの関数宣言形式が、古い形式で書かれているからです。 つまり、 void* memset(void* s, int c, size_t n); ではなく、 char* memset(); と宣言されています。 C言語はC++言語と違い、引数リストが空で宣言されている関数は、 実際の定義や使用時に、どんな引数を指定してもエラーになりません。 この場合がまさにそれに当ります。 同じくstring.hで宣言されているstrcpy()やstrlen()など多数の関数、 time.hで宣言されている関数も、どんな引数を指定しても コンパイルエラーにならないので、注意が必要です。 特にstring.hで宣言されている関数は、多用するものばかりなので、 引数を間違う可能性も高く、非常に危険です。 ところでmemset()の宣言、戻り値も違っているようですが、 昔のmemset()はvoid*じゃなくってchar*を返していたのでしょうか? 手元のK&R第2版では、既にvoid*を返すようになっていますが... * Sat Dec 22 01:09:08 JST 2001 Naoyuki Sawa - BIOS ver1.14 今週末もP/ECEのアップデートがありました。 こんな手厚いサポートして、メーカーは大丈夫なんでしょうか? 余計なお世話でしょうが、心配してしまいます。 今回のアップデートで、P/ECEコミュニケータがドラッグ&ドロップに対応した模様。 僕のmini ISDは、用済みとなってしまいました。 あとは、P/ECEからWindowsへのドラッグ&ドロップを実装して差別化するしかないのですが、 アプリケーションからエクスプローラへのドラッグ&ドロップって、やり方知らない...^^; う〜む、ちょっと勉強が必要です。 BIOS ver1.14からBIOS ver1.14へのアップデート(つまり同一バージョンの上書き)時に、 アヤシイ振る舞いがありましたので、トラブルシュート掲示板に報告しました。 P/ECE上のファイルを全て消さないと、同一バージョンの上書きに失敗するというものです。 うちだけの現象ならいいのですが。 * Wed Dec 19 17:18:36 JST 2001 Naoyuki Sawa - __FILE__マクロが正しく展開されない pcc33は、__FILE__マクロを正しく展開しない模様です。 例えば、こんな関数を含むプログラム... const char* test() { return __FILE__; } を、 pcc33 -c sample.c でコンパイルしてみると、 "sample.c"ではなく"sample.$"に展開されます。 pcc33は内部でgcc33を呼び出しているので、 よりピュアなgcc33を直接使って gcc33 -c sample.c としてみると、ちゃんと"sample.c"に展開されます。 たぶん、pcc33が前処理を行った一時ファイル、 sample.$に対してgcc33が呼び出されているのでしょうね。 実際にはgcc33を直接使うことは少ないと思うので、 当面、ランタイムエラーメッセージなどに表示される ファイル名がおかしいという点は我慢せざるを得ないようです。 あ、いま気が付きましたが、前処理を行った後のファイルで __FILE__が展開されているのなら、きっと__LINE__も同じかな? もしそうならば、行番号が変わるのはきびしいなぁ... 要、調査です。 * Sun Dec 16 20:00:58 JST 2001 Naoyuki Sawa - ソルダム/セキーロ そろそろ実際にP/ECEでゲームを作らねば。 まずは研究という名目で、ソルダム/セキーロ(JARECO 1992)で遊ぶ。 遊ぶ遊ぶ。おもしれ〜。 このゲーム、非常にマイナーな部類に入りますが、 ONE OF MY BEST 落ちものパズルゲームなのです。 落ちものパズルというと、右も左も連鎖・連鎖の風潮であった当時、 わが道を行く地道で淡々としたゲーム性、 スピーディなソルダムと、じっくり攻略セキーロ、の2モード。 十年近く経った今でも、まったく色あせておりません。 まあ、90%ぐらい曲依存ゲーなのは否めませんが... 皆様も街中で見かけたら、ぜひ遊んでみることをお勧めします。 まず見かけないけど^^; さて、もう1ゲーム行きますか。...はっ!僕は何をやってるんだ? * Sun Dec 16 09:04:57 JST 2001 Naoyuki Sawa - NULLポインタへの書き込み P/ECEのCPU、S1C33にはメモリ保護機能がないので、 テキトーなアドレスを読み書きしても、その場ですぐに 「アドレス0x12345678がReadになれませんでした。」(by Win2K) というようなエラーは表示されません。 後々、じわじわと効果が(悪い効果ですが)現れてきます。 これではとてもデバッグがやりづらい... むちゃくちゃなアドレスへの書き込み *(char*)0x00000123 = 45; や、NULLポインタの読み出し a = *(char*)NULL を検出する方法はちょっと思いつかないので、まずは NULLポインタへの書き込みだけでも検出することにしましょう。 int save000; void pceAppInit() { save000 = *(int*)NULL; ... } void pceAppProc() { ... if(*(int*)NULL != save000) die("Write address 0"); } ちなみに現在のカーネルでは、0000〜0005の位置には、 NULLポインタ呼び出し検出用のジャンプ命令が置かれています。 このうち0000〜0003を保存しておいて、書き換えられていないか 時々チェックするわけです。 S1C33と同様にメモリ保護機能を持たない、 8086やZ80用のCコンパイラが採っていた方法を参考にしました。 * Fri Dec 14 21:30:49 JST 2001 Naoyuki Sawa - 逐次処理プログラム 僕はズボラーなプログラマなので、main()だけで全部やりたいのです。 もっとgotoを使いたいのです〜! というわけで、逐次処理プログラムのサンプルです。 http://www.piece-me.org/archive/sample-20011214.c オフィシャル開発掲示板にて発言させていただいたところ、 「main()があると安心する」というご意見をいただきました。 そういえば、main()の正しい定義は、 int main(); または int main(int argc, char* argv[]); または int main(int argc, char* argv[], char* envp[]); ですが、上記のサンプルでは void main(); としていました。これはハズカシイので、さっそく治したのがこちら。 http://www.piece-me.org/archive/sample-20011214a.c main()からの戻り値が、pceAppReqExit()への引数になります。 もっとも、現在のところpceAppReqExit()への引数は無視されており、 つまり、全く無意味な修正です^^; あ、別にmain()から制御を返さなくても、 int main() { /* いろいろ処理 */ pceAppReqExit(終了コード); yield(); /* これでおしまい */ /* ここへは来ません */ } これで終了できます。 #define exit(n) pceAppReqExit(n); yield(); と定義しておくと、ちょっといいかもしれません。 * Wed Dec 12 23:10:35 JST 2001 Naoyuki Sawa - 強制リセット(つづき) エラーメッセージを表示して停止する関数の実装例です。 STARTボタンとSELECTボタンを押すと、メニューに戻ります。 void die(const char* fmt, ...) { #ifdef PIECE asm(" xcall pceLCDDispStop ; pceLCDDispStop(); xld.w %r12, _def_vbuff ; pceLCDSetBuffer(_def_vbuff); xcall pceLCDSetBuffer xcall pceLCDDispStart ; pceLCDDispStart(); xld.w %r12, 2 ; pceFontSetType(2); xcall pceFontSetType xld.w %r12, 3 ; pceFontSetTxColor(3); xcall pceFontSetTxColor xld.w %r12, 0 ; pceFontSetBkColor(0); xcall pceFontSetBkColor xld.w %r12, 0 ; pceFontSetPos(0, 0); xld.w %r13, 0 xcall pceFontSetPos xld.w %r4, die_L10 ; pceFontPrintf(fmt, ...); xld.w [%sp], %r4 xjp pceFontPrintf die_L10: xcall pceLCDTrans ; pceFontLCDTrans(); die_L20: ; do { xcall pcePadGetDirect ; pad = pcePadGetDirect(); xcmp %r10, 0xC0 ; } while(pad != (PAD_C | PAD_D)); jrne die_L20 xld.w %r4, [0xC00000] ; system reset jp %r4 "); #else /*PIECE*/ va_list ap; va_start(ap, fmt); fprintf(stderr, fmt, ap); fprintf(stderr, "\n"); va_end(ap); abort(); #endif /*PIECE*/ } * Tue Dec 11 22:06:04 JST 2001 Naoyuki Sawa - 強制リセット SELECT+STARTの0.5秒長押しでかかるリセットは、 実はリセットではありません。 カーネルがSELECT+STARTの0.5秒長押しを検出したら、 アプリケーションの代わりにpceAppExit()を呼んでいるだけです。 つまり、全く普通のアプリケーション終了手続きとなります。 アプリケーション内部で予期しないエラーを検出した場合など、 強制リセットをかけたいことがあります。 APIにはそのような機能がないようなので、 直接リセットベクタへ飛ぶことにしました。 asm("xld.w %r4, [0xC00000]"); asm("jp %r4"); ただし、これでは周辺回路がリセットされないので、 多少危険なのですが、いまのところちゃんと動いています。 P/ECEカーネルは周辺回路のコールドリセット状態を仮定せず、 きちんと再初期化してくれているみたいです。 * Tue Dec 11 21:53:29 JST 2001 Naoyuki Sawa - アイテムフルコンプリートです ゆうべ、アイテムフルコンプリートしました。 これのどこがおもしろかったのか、じぶんでもわからないのですが、 ともかく、なぜか、とても、とても、たのしいゲームでした。 ぼくも、こんなたのしいゲームがつくれるプログラマに なりたいです。 * Mon Dec 10 12:25:03 JST 2001 Naoyuki Sawa - 極小フォント pceFontSetTypeの引数は、マニュアルには、 0: 5x10 通常フォント (日本語あり) 1: 8x16 拡大フォント (日本語なし) しか載っていないが、 2: 4x 6 極小フォント (日本語なし) もある。 TRAP画面でレジスタダンプに使うためのフォントのようだが、 ユーザーアプリケーションでも普通に使える。使おう。 ただし日本語はない(半角カナもない。これはタイプ1も同様)。 スコア表示などにちょうど良いかもしれない。 * Sun Dec 9 02:18:55 JST 2001 Naoyuki Sawa - 空白を含むファイル名は削除できいない 書き込みはできる(ismPFFSWrite)が、削除できない(ismPFFSDelete)。 システムアップデートで無理やり消すしかなくなる。危険! * Sat Dec 8 19:52:24 JST 2001 Naoyuki Sawa - APIリファレンス誤植 pclSpriteBGClearの説明で、 「bg1のプレーン(擬似コンソールとしての文字表示用プレーン)の、 コンソール範囲のキャラクタをクリアします。」とあるが、 bg0の間違いではないか。(1.12a) * Sat Dec 8 19:42:43 JST 2001 Naoyuki Sawa - stddef.hはインクルードしない stddef.hをインクルードすると、wchar_tの多重定義エラーになる。 原因不明。 そういえば、S5U1C33000C_J.pdfの4ページの一覧にも含まれていない。 あまり重要なヘッダファイルではないようなので、外して様子見。 * Sat Dec 8 19:35:14 JST 2001 Naoyuki Sawa - 便利なシステム定数 DISP_X = 128 画面の横ピクセル数 DISP_Y = 88 画面の縦ピクセル数 BG_CX = 32 BGの横キャラ数 BG_CY = 32 BGの縦キャラ数 * Sat Dec 8 18:50:03 JST 2001 Naoyuki Sawa - va_start()について stdarg.hをインクルードしただけでは、_BOUNDARYが未定義になる。 smcvals.hをインクルードしなければいけない。 S1C33アーキテクチャ依存のビット数などが定義されているようだ。 * Sat Dec 8 18:50:43 JST 2001 - errnoが未定義になる ユーザープログラム中で定義しなければいけない。 BlackWingsやタンクバトルのソースでも、そうやってる。 S1C33 Family C Compiler Package の仕様だ。 S5U1C33000C_J.pdfの104ページを参照。 ただし、errnoを変更する可能性のある標準関数を使わなければ、 (例えば、P/ECE専用ライブラリ関数だけを使っていれば) そもそもerrnoが必要でないので、未定義エラーにはならない。