+===========+ |P/ECE研究室| +===========+ P/ECE研究記録 2015年 ==================== * 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