ダブルバッファーで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」カテゴリの記事
- 心電計プロジェクト:スケールが出ると心電計らしくなる(2015.01.08)
- 心電計プロジェクト:TFT液晶に念願の心電波形が出た(2014.12.18)
- 心電計プロジェクト:STM32F103の心電波形表示で悪戦苦闘(2014.12.03)
- 心電計プロジェクト:CooCoxでARMの表示系ソフトを開発する(2014.10.16)
- 心電計プロジェクト:表示部のARM基板の開発環境を一新する(2014.09.19)
コメント
ご丁寧に報告ありがとうございました。このブログの以前の記事を読んでいただければわかると思いますが、USB_Putcなどで、送信バッファーに連続して多数のデータを送り込んでも不具合が起きないようにしたのが、ダブルバッファー方式です。tagさんのように、あらかじめnakにしておいて、データを溜めてから送信していれば、確かにダブルバッファーは必要ありません。そういうプログラムの作り方もありですが、連続して送るより速度は遅くなりますね(tコマンドで送信速度を測れます)。
投稿: がた老 | 2010年6月19日 (土) 21時18分
今までの感触で「ダブルバッファでなくても
うまくUSBのIN転送ができるように」思えたのは、
うまく手加減ができていたからということが
よくわかりました。
OUT転送で受け取った時だけデータを用意して
IN転送するようなやりかたでは
全く問題ないのでしょう。
(あるPCで問題ありと書いたのはUSB HUBに
さしていたのと
他のポートにもデバイスが刺さっていたため。)
大変ご迷惑をおかけしました。
投稿: tag | 2010年6月18日 (金) 23時29分
すみません、計測を早くするため
64byteごとの転送にしたもので試したのを書き忘れてました。
nak状態に設定するのも無意味とわかりました。
ただ、あるPCでは全く問題ないのですが、
違うPCでは問題となります(原因不明)。
なかなか難しいですね。
投稿: tag | 2010年6月15日 (火) 01時09分
tagさんテストありがとうございました。ほほう、問題ない。何故だろう。
USBライブラリはV1のままだし、ride7は開発環境で関係ないし、どうしてだろうと、もう一度ソースを見させていただきました。
その結果何となく原因が、わかってきたような。
tagさんは、USB_Putline(tコマンドでも使っている)を以下のように修正されておられますが、
void USB_PutLine( unsigned char * string)
{
while(count_in); //クリアされるまでは待ち
while(*string)
{ USB_Putc( *string++); }
USB_SIL_Write(EP1_IN, buffer_in, count_in);
SetEPTxValid(ENDP1); //送信(in転送)可能となったのでvalidに
}
これは、エンドポイントの送信バッファーがなくなるまで待ち(空きを待って)、stringが終わったところで、UserToPMABufferCopyを使ってエンドポイントに書き込んでいます。tコマンドの文字数がたまたま10文字程度だったので破綻しなかったのでしょう。これはこれで良いでしょうが、このままだと一文字関数USB_Putcは出した後で、SetEPTxValidかなにかでエンドポイントを叩かないと文字が出て行きませんね。
こちらは今STM32から遠ざかっていますが、近々再開する予定です。そのときはこちらでももう少し詳しく調べてみます。ありがとうございました。
投稿: がた老 | 2010年6月13日 (日) 18時54分
アドバイスありがとうございます。
早速やってみました。
t2を100回繰り返しましたが問題ありませんでした。
しかも、私がやったnak状態に設定するところを
削除にしてさらに100回繰り返しをやりましたが
大丈夫なんですよ、何故か。
そうなると絶対必要なのは送信するときの
「SetEPTxValid(ENDP1)」だけのような感じになります。
でも、これはがた老さんもされていたことですし。
ますますわからなくなってきました。
Ride7を使ってることとか、コンパイルオプションあたりに
影響されてる可能性もあるのかな?
とりあえず問題はおきてないからそのままやってても
いいのかもしれません。
もう少し様子を見てみます。
投稿: tag | 2010年6月13日 (日) 17時46分
tagさん、レスポンスありがとうございました。
ソースをもう少し見てみたら、私のモニタープログラムがそのままのようなので、これで動かしているなら、これまでのことを確認する方法があります。
コマンドtは、n×1000文字を一気にUARTに送って、送信抜けがないかテストするものです。t1とかt2と入力し、リターンキーを押します。1234567890という文字列が正しい量だけPCに表示されたら、ダブルバッファーなしに送れていることになります。お試しください。
投稿: がた老 | 2010年6月12日 (土) 18時53分
ご回答いただきありがとうございます。
nak状態にしているときは
EP1_IN_Callback()が発生しないので、
割り込みが入ってないと思います。
でもおっしゃるとおり0バイト送信を
やめただけでしかないのかもしれませんね。
また、私の使い方では問題が起こらないという
可能性もあるかも。
とりあえず様子を見てみます。
ありがとうございました。
投稿: tag | 2010年6月12日 (土) 17時39分
tagさん、コメントありがとうございました。
ダブルバッファーにしてあるのは、大量のデータをエンドポイントに放り込むとき、途中で起きるハードの割込みを回避するためなのですが、tagさんのnak処理でこの問題は解決しているのでしょうか。少しソースを見させていただきましたが、良く分かりませんでした。どうされているのか教えていただけると幸いです。ループバックでは、UARTの速度に制限されるので不具合は表面化しません。nakは、0バイトを送信する「空振り」を止めているだけのような気がしますが。
投稿: がた老 | 2010年6月12日 (土) 12時50分
いまごろになってDWM付録STM32を動かすため、
参考にさせてもらってます。
ところで、ダブルバッファでなくても
うまくUSBのIN転送ができるようになりました、多分。
http://omoitsuki-teck.cocolog-nifty.com/blog/2010/06/stm32uusbd-ca90.html
にのせてみました。
よろしかったらご覧ください。
投稿: tag | 2010年6月12日 (土) 00時50分
みなさんコメントありがとうございます。
引数にもvolatile宣言が出来るとは知りませんでした。
「ねむい」さん、早速実験をして頂き感激です。V3は構成がかなり変わっていて二の足を踏んでいました。安価なUSB-UARTの石が出ているので、VCPのためにSTM32を使う気にはなりませんが、これで230kbpsクラスのUARTも大丈夫です。
投稿: がた老 | 2009年6月 3日 (水) 11時27分
V3.0.1のファームエアライブラリに移植して試したところ、
問題なく動きましたのでご報告します。
投稿: ねむい | 2009年6月 2日 (火) 22時24分
Delay( int i) { while(i) i--; }
これが無効なコードとされてしまうのは最適化のせいではないでしょうか? コンパイラのオプションに "-Ox" (xには数字が入ります)を指定していませんか?
最適化の方法として "ループアンローリング" というものがあり、これが起因しているのではないかと思われます。
簡単な回避方法として、ループの条件式部分に使っている変数(この場合 i) を volatile宣言してやれば ループアンローリングの対称から逃れる事ができます。
投稿: jujurou | 2009年6月 1日 (月) 22時38分