ダブルバッファーでUSB仮想UARTの内部速度が2Mbps
しゃれた実装を考えたが挫折(5/28/09)
SysTickタイマーで1ms単位の経過時間が測れるようになったので、勇躍、STM32基板のUSB仮想UARTの高速化にとりかかった。これまでにわかっていることは、UARTの速度が遅いときは問題ないが、早くなってくるとエンドポイントの送信終了の割込みがユーザープログラムがデータを移している最中に入り、このときのデータサイズが実際のデータと不整合が生じてデータが欠落する。
これはSTマイクロが提供するSTM32F(Cortex-M3)用のVCPデモプログラムに最初からあった問題で、STマイクロのフォーラムで去年話題になり議論されていた。少なくとも私の知る限りまだ解決していないはずである。SysTickを使って改修されたコードがアップされたが、その後本人が動かないとして取り下げている。この解決を当研究所がやろうというのである。もしうまく行けば鼻が高い。
方式はダブルバッファー方式である。前のDACのデコードと同じような考え方だ。ひとつのバッファーをユーザーの送信関数の書き込み用とし、ここに送信データを入れていく。送信関数ではエンドポイントに直接書き込まない。ここで書き込むと、書き込み終了の割り込みがどこで起きるかわからないからである。もうひとつのバッファーは、割込みルーチンが既に書き込み処理をしたデータが入っている。
書き込み終了の割込みがかかり、割込みルーチンに制御が渡ったときに、バッファーをスイッチして、今度は送信関数が書き溜めていたデータをエンドポイントに書き込む。さきほど送り終わったバッファーが今度は送信関数が新たに書き込むバッファーとなる。これで、データ書き込みと送り出しの衝突を避けることができる。
最初、コードを少なくするため、2組必要なバッファー配列とカウンターをポインター変数にし、そのアドレスをスイッチして動くようなスマートな方式でコードを組んだ。
バッファーに送信バイトを送り込むとして、
* ( バッファーアドレス + * カウンター++ )= 送信バイト;
みたいなプロっぽいコードである。バッファーアドレスとカウンターポインターを取り替えるだけで、main.cの送信関数USB_Putc()はダブルバッファーであることを知らないで済む。いやコーディング技術も格段に進歩したものだ、と、得意になって完成させた。ロジックはそう難しいものではない。気をつけないといけないところは、バッファーを切り替えるときに、ユーザープログラムの実行と重ならないように、フラグを立てて守るということである。今度は、割込みルーチンが見えているので、ユーザー側は、そのフラグを見て待てばよい。
と、これが全くうんともすんとも言わないのである。どうもさっきのステートメントのところで暴走している。表記に間違いはないはずだ。何度もウェブなどで確かめる。うーむ、何か勘違いがあるのだろうか。プログラムはまさしく考えたようには動かない。書いたように動くだけだ。いずれにしても、ポインターを使ったコーディングは一歩間違えば、とんでもないところにデータを書き込み、簡単に暴走してしまう。
色々いじったが好転しない。悩んだ挙句、この方法をとりあえずあきらめ、普通の配列を使い、愚直に2組のバッファーを別々に操作するルーチンに切り替えた。さあ、これでどうだ。これなら暴走しようがない。もう問題ないはずだ。
ありゃりゃあ、これでも動かない。ええー、どこが悪いんだ? シングルバッファーに戻すと何事もなく動く。わからない。このあたりでGDBが使えれば良いのだが、今のままでは、USBを使っているので初期化の段階で、HardFaultに落ちてしまいメインループに来ない。環境を大きく換えないと使えない。別のUARTで変数を出してデバッグする手もあるが、どちらも手間がかかる。
ダブルバッファーが動いた!しかしこれで良いのか(5/29/09)
開発が暗礁に乗り上げている。マラソンや登山と同じだ。この苦しみが歓喜の材料なのだが、その最中はそれどころではない。大げさに言えば人生が暗い。今まで生きてきたことすべてが意味のない時間だったような気分になる。
BeagleBoardのために久しぶりにモニターを取り替え、1920x1080のワイドディスプレイになって開発環境は一変し(エディターと端末が同時に見られる)、快適になったのだが、気分は優れない。救いは、このあいだの第三のファーム書き込み手法ROMのフラッシュローダーが思いのほか使い勝手が良く、プログラムの修正、ビルド、ファーム書き込みのサイクルが飛躍的に早くなったことである。AVRなみの開発環境である。Eclipseは編集とビルドだけにしか使っていないが、これはこれで便利だ。
それはともかく、ダブルバッファーが動かない。基本的なところを少しづつ確認していくしかない。何回目かのコードレビューの最中に、ふっと気がついた。送信関数は律義にバッファーにデータを溜め込み、一杯になれば、割込みルーチンがバッ
ファーを切り替えるのを待っている。では誰がUSBのエンドポイントに書き込むのだ。あ、あー、ここだ。エンドポイントに書き込む処理は、エンドポイントに書いた後の割込みルーチンにしかない。ポインターの暴走でもなんでもない。元から動かない。
何と言うお馬鹿なコードだ。これでは永遠に送れない。しかし、どうすれば良いのだろう。頭の中が混乱する。何か根本的な間違いがあるのかもしれないが、ロジック的には一度mainの初期化のところでエンドポイントにデータを書き込む呼び水のようなダミー処理をすれば良いのではないか。そうすれば次々に割り込みが連続し、うまくいくのではないか。
やってみた。やっほー、動いたぞ。ダブルバッファーの仮想UARTモニターが前と同じように動き始めた。転送速度を測る。ややや、2Mbpsだ。素晴らしく早い。1KB送るのに数msしかかかっていない。そりゃそうだ。バッファーに1バイトづつ送るだけで60kbps近くあったのだから、その64倍だったら3Mbps近くあっても、おかしくない。
もちろんこの速度は、単に内部でUSBドライバーが受け取った時の速さで、実際にホスト(PC)との仮想UARTでの転送速度ではない。しかし少なくとも内部では安定的にメガビットのオーダーでデータが送れたことになる。
肝心のデータ欠落をチェックするために、同じ文字でなく、"0123456789"の文字列を送ってみる。うむ、全く欠落はない。きれいに数列が画面上に並ぶ。1字の落ちもない。いやあ、至福の瞬間である。今までの暗い気持ちが吹き飛び、天下をとった気分になる(相当中毒が進んでいる)。
しかし、ロジックとしては不満がある。今の状態は、データがなくてもエンドポイントに立て続けに書き込む処理が動いているはずだ。今は単なるモニタープログラムなので関係ないが、別の
仕事をするときはCPUリソースを無駄に使っていることになり効率が悪い。
というので、エンドポイントの割込みルーチンにデータがなければ処理をやめるようにし(割込みが止まる)、送信関数には、バッファーが何もないときのデータ送信では、ダミーのエンドポイント書き込みを入れるようにしたが、こいつはまた全く動かない。
やっと満足できるコードになった(5/31/09)
速度が2MbpsまでになったUSB仮想UARTだが不満が残っている。呼び水方式で、USBのエンドポイントにデータを送るところまでは良かったが、これでは送信していないときも延々とUSBドライバーは0データを送り続ける。
送信データがないときはこの繰り返しを止め、データが来たときに再開するロジックを色々考えて試してみるが、ことごとく失敗する。奇怪なことに、メインルーチンの初期化でダミーの書き込みをすると上手く行くのに、送信関数の中でやると先に行かない。
この間の差は時間だけだ。余りやりたくなかったがタイマーを入れて調整することにした。しかし全く変わりがない。何か変だ。このタイマー(元々の雑誌のソースに入っていたもの)は
Delay( int i) { while(i) i--; }
という簡単なコードだが、iをいくら増やしても変わりがない。コンパイラーの最適化を疑ったが、i--は立派な処理だ。無効になるわけがない。それにこの関数は、前から使っていたはずだ。
しかし、やっぱりおかしい。念のため、コマンドを新設してこいつの待ち時間をSysTickの時間で測ってみた。うひゃー、全部0で帰ってくる。何と言うことだ。コンパイラーがこのステップを取ってしまっているに違いない。
AVRでやったように、ループにasm volatile("NOP");を入れる(これはアセンブラーコードでなくマクロだそうだ。従ってARMでも共通)。ちゃんと待ち時間遅れが作れた。やっぱりコンパイラーの仕業だ。 雑誌で動いていたのは開発環境IARとGNUの違いだろう。それにしても、i-- がどうして無駄なコードなのだ。理解に苦しむが文句を言っても始まらない。
ところが、待ち時間を作ってもうまく行くときと行かないときがある。時間待ちはあくまでも対症療法だ。やはり基本から確実な方法を探すしかない。もういちどソースコードをひとつひとつ追いかけて手順を考えることにした。
BeagleBoardのセットアップ(次記事で紹介)で少し日をあけたのが良かったのだろう。良い方法を思いついた。これまで送信関数の方ばかりに注目して、何とかしようとあれこれ考えていた。しかし、割込みルーチン側で動作モードを設定していけばうまく行くことに気がついた。そうなのだ。送信関数は、いつどこで割込みを受けるかわからないのでデータカウントが変わる可能性がある。バッファーを制御するスイッチにもうひとつ「データなし」というステイタスを追加してプログラムを組み直した。祈る気持ちでテストする。やった。前と変わりなくデータが送信できた。
CPUのオーバーヘッドが明らかに減っている。その証拠に先のループで待つウェイトルーチンの時間が早くなったのだ。すごい。はっきりと差がわかる。時間にして3%くらい早くなっている。やっとまともなコードになったと思う。
ソースコードの公開は迷った。単に大量送信の時間が表示されるだけの、このままでは実用性0(ゼロ)のモニタープログラムである。まあ、これをベースに色々アプリケーションを考えれば役に立たないわけでもない。がた老「AVR」研究所の記念すべき、初の「ARM」プログラムソースでもある。
人のソースを流用させてもらっているが、殆どのソースは雑誌からなので問題ないだろう。STマイクロのデモプログラムも、ソースリストに「責任取らないからね」という文言しかないので問題ないと判断した。Makefile、リンカースクリプトとスタートアップルーチンはこちらのお世話になった。この場を借りて御礼申し上げたい。
ソースファイル一式(ライブラリ、リンカースクリプト、Makfile)をここにおきます。Eclipseのプロジェクトファイルですが、Eclipseがなくても動きます。また、実行させるだけなら、STM_VCPDフォルダーの中のstm_vcpd.hexをDfuか、フラッシュローダーで書き込めば動きます。
コンパイルの簡単な手順は以下の通り。
・コンパイラー(CodeSourcery G++)をここからダウンロードする。(OSはEABI、Sourcery G++ Lite 2009q1-161を選ぶ。 5/31/09現在)
・解凍したフォルダの下のSTM_VCPDフォルダーにカレントディレクトリを置き、DOS窓でコンパイル(make all)する。 FWLibや、USBLibのディレクトリの位置は変えないこと。換える場合は、Makefileを修正する。
・また、gccの標準ライブラリとヘッダーファイルは、gccの入っているbinフォルダーと同列のarm-none-eabiフォルダーの中のlib\thumb2下のライブラリと、include下のヘッダーを使っている。正確にはMakefileを参照。
・解凍ファイルの中のFWLibや、USBLibはV1.0で、現在のV3と混在させると動かない可能性がある。
・出来上がったhexファイルをDfuか、フラッシュローダーでファームに書き込む。
・STM32の電源を入れ、PC側の端末プログラムでUSBのVCPに接続し、何かキーを入力すると、WelComeメッセージが出る。
| 固定リンク
「ARM」カテゴリの記事
- LPCMプレーヤー2号機開発の道草(2009.07.16)
- STM32基板のRTCを動かす(2009.06.08)
- STM32基板のUSB仮想COMポートがGNUで動いた(2009.05.04)
- SysTickを使ったSTM32のUSB仮想UARTの速度測定(2009.05.28)
- ダブルバッファーでUSB仮想UARTの内部速度が2Mbps(2009.06.01)

コメント
Delay( int i) { while(i) i--; }
これが無効なコードとされてしまうのは最適化のせいではないでしょうか? コンパイラのオプションに "-Ox" (xには数字が入ります)を指定していませんか?
最適化の方法として "ループアンローリング" というものがあり、これが起因しているのではないかと思われます。
簡単な回避方法として、ループの条件式部分に使っている変数(この場合 i) を volatile宣言してやれば ループアンローリングの対称から逃れる事ができます。
投稿: jujurou | 2009年6月 1日 (月) 22時38分
V3.0.1のファームエアライブラリに移植して試したところ、
問題なく動きましたのでご報告します。
投稿: ねむい | 2009年6月 2日 (火) 22時24分
みなさんコメントありがとうございます。
引数にもvolatile宣言が出来るとは知りませんでした。
「ねむい」さん、早速実験をして頂き感激です。V3は構成がかなり変わっていて二の足を踏んでいました。安価なUSB-UARTの石が出ているので、VCPのためにSTM32を使う気にはなりませんが、これで230kbpsクラスのUARTも大丈夫です。
投稿: がた老 | 2009年6月 3日 (水) 11時27分