USBUART の TX に FIFO 機能を装備する [PSoC]
PSoC Advent Calendar 2016の12日目の記事です。
PSoC 5LP の USB インターフェイスを使うには、 USBUART コンポーネントを使った仮想 COM ポートを利用するのが簡単です。 ところが、 USBUART コンポーネントは、他の UART などのシリアルインターフェイスが持っているような FIFO を持っていません。 この記事では、 USBUART で FIFO を使う方法をご紹介します。 まずは、 TX 側から。
USBのややこしい所
USBUART には、1バイトのデータを送るための PutChar() メソッドが定義されています。 このメソッドを使うと、エンドポイントの送信が終わったのを確認してから、エンドポイントバッファに1バイトのデータを入れます。 そして、ホストから取り込み要求が来た時にバッファのデータを送信します。 エンドポイントバッファは64バイトの大きさがあるのですが、 PutChar() メソッドを呼び出した時には1バイトしか使われません。
データ送信のスループットを上げるためには、64バイトのバッファになるべく多くのデータを入れなくてはなりません。 そこで考えられるのが、64バイトのデータが貯まるまで送信を遅らせる方法ですが、この方法ではデータが先方に届くまでの遅延時間が大きくなってしまいます。 例えば、63バイトのデータを送りたい場合、永遠にデータが送りだされない可能性もあります。
このような事態を防ぐため、ここで作成する FIFO では、以下の方針を取り入れました。
- 送りたいデータは、バッファに積んでおく。
- 周期的にバッファを監視して、送信可能な状態であればバッファからデータを送信する。
ここで重要なのは、「バッファの監視周期」です。 USB Full-Speed では、 1ms のフレームと呼ばれる時間単位で区切られており、このフレームにパケットを詰めます。 ここでは、ひとつのフレームに必ずひとつ以上のパケットを入れられるように監視周期をフレームの半分の時間 0.5ms に設定しています。 具体的には、 2kHz のクロックにより割り込み "int_uartQueue" を発行しています。
クロックの設定
クロックは、 USB バスからクロック成分を取り出して Internal Main Oscillator (IMO) の周波数を微調整する方式を採用しています。 これにより、 IMO の周波数は 24MHz±0.25% とすることができます。 CPU 等を駆動するためのクロック BUS_CLK は、システムでの最大周波数 79.5MHz を PLL により生成し使用しています。
ファームウェア
ファームウェアは、以下のようになりました。
#include "project.h" // FIFO 機能のON/OFF //#define NOFIFO
冒頭、 include に続いてコメントアウトされた NOFIFO マクロ宣言があります。 このマクロを有効にすると、 FIFO を使わない設定も試す事ができます。
// USBUARTのパケットサイズ #define UART_TX_QUEUE_SIZE (64) // USBUARTのTXキューバッファ uint8 uartTxQueue[UART_TX_QUEUE_SIZE]; // TXキュー uint8 uartTxCount = 0; // TXキューに存在するデータ数 CYBIT uartZlpRequired = 0; // 要ZLPフラグ uint8 uartTxReject = 0; // 送信不可回数
USBUART で使用する BULK パケットのサイズと FIFO で使用する変数を宣言しています。 USBUART コンポーネントには、ディスクリプタが含まれているのだから、パケットのサイズぐらい引っ張り出せそうなものなのですが、適当な方法が見つからなかったので、再定義しています。 このパケットサイズを条件としてキューバッファにデータを貯めておき、頃合いを見計らって USBUART コンポーネントから送り出します。
「要ZLPフラグ」は、 Zero Length Packet (ZLP: 長さゼロのパケット)が必要な状況であるかどうかを示します。 パケットの最大サイズは64バイトです。 この大きさを超えるデータを送りたい場合、複数の64バイトのパケットに続いて64バイト未満のパケット(Short Packet: ショートパケット)を送り出します。 このショートパケットでデータの終端を表すのです。
ところが、64バイトの倍数の長さのデータを送る場合には、すべてのパケットのサイズが64バイトなので、データの終端が見分けられません。 このような場合、データの終端を表すために使用されるのが ZLP です。 「要ZLPフラグ」は、データの最後のパケットが64バイトであった場合にセットされ、次回 ZLP を送信する必要があるかどうかを判断します。
バッファに貯まったデータを USB パケットとして送信する時、 USBUART コンポーネントの受け入れ準備が出来ていなければ、次の機会を待つことになります。 このような後回しの状態が続くとデータを送信したいアプリケーション側の処理が滞ってしまいます。 この例では、処理の停滞を防ぐために、受け入れを拒否された回数を uartTxReject で数えておき、繰り返し受け入れを拒否された場合にはバッファのデータを廃棄して処理を先に進ませるようにしています。
#ifdef NOFIFO // 1バイトを送信する関数 static void putch_sub(const int16 ch) { // FIFOを使わない時は、PutChar()をそのまま使う USBUART_PutChar(ch); }
FIFO を使わない場合、1バイトのデータを送るには USBUART_PutChar() 関数を使います。
#else // define(NOFIFO) // 1バイトを送信する関数 static void putch_sub(const int16 ch) { uint8 state; for (;;) { // 送信キューが空くまで待つ state = CyEnterCriticalSection(); if (uartTxCount < UART_TX_QUEUE_SIZE) break; CyExitCriticalSection(state); } // 送信キューに一文字入れる uartTxQueue[uartTxCount++] = ch; CyExitCriticalSection(state); }
一方、 FIFO を使う場合には、送信キューに文字を積んでいきます。 もし、送信キューに空きが無かったら、空きが出来るまで待ちます。
送信キューに空きができるのは、周期割り込みによりバッファを送り出した時です。 つまり、この関数の実行中には割り込みがかかる事が期待されており、タイミングによっては、送信キューを構成する変数などが意図せず書き換えられる可能性があります。
このような事態を防ぐためには、 Critical Section (きわどい領域)という他のプログラムの介入を禁止する区間をつくって、変数などを保護してやります。 この Critical Section を確保するための関数が CyEnterCriticalSection() と CyExitCriticalSection() です。 これらの関数を使う事で、安全に割り込みを禁止する事ができます。
// 送信側割り込みサービス制御 void uartTxIsr(void) { uint8 state = CyEnterCriticalSection(); if ((uartTxCount > 0) || uartZlpRequired) { // バッファにデータが存在する、または、ZLPが必要な時にパケットを送る if (USBUART_CDCIsReady()) { // 送信可能なら - パケットを送る USBUART_PutData(uartTxQueue, uartTxCount); // バッファをクリアする uartZlpRequired = (uartTxCount == UART_TX_QUEUE_SIZE); uartTxCount = 0; uartTxReject = 0; } else if (++uartTxReject > 4) { // 送信不可が続いたら - バッファのデータを棄てる uartTxCount = 0; uartTxReject = 0; } else { // 次回に期待 } } CyExitCriticalSection(state); } #endif // define(NOFIFO)
周期割り込みの処理ルーチンでは、 USB のパケットを送信しています。 この処理も Critical Section に入れてあります。 これは、周期割り込みよりも優先順位の高い割り込みからデータ送信関数が呼ばれる場合を想定したものです。
// USBUARTに一文字送る void putch(const int16 ch) { if (ch == '\n') { // LFをCRLFに変換する putch_sub('\r'); } putch_sub(ch); }
実際にアプリケーションから呼ばれる一文字送信関数は、 putch() です。 この関数の中では、 LF を CRLF に変換する処理が入っています。
// USBUARTに文字列を送り込む void putstr(const char *s) { // 行末まで表示する while (*s) { putch(*s++); } } // 32-bit十進数表 static const uint32 CYCODE pow10_32[] = { 0L, 1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, }; // 32-bit数値の十進表示 - ZERO SUPPRESS は省略。 void putdec32(uint32 num, const uint8 nDigits) { uint8 i; uint8 k; CYBIT show = 0; // 表示すべき桁数 i = sizeof pow10_32 / sizeof pow10_32[0]; while (--i > 0) { // 一の位まで表示する // i桁目の数値を得る for (k = 0; num >= pow10_32[i]; k++) { num -= pow10_32[i]; } // 表示すべきか判断する show = show || (i <= nDigits) || (k != 0); // 必要なら表示する if (show) { putch(k + '0'); // 着目桁の表示 } } }
出力処理を行うユーティリティ関数として、文字列を出力する putstr() と十進数を表示する putdec32() を用意しました。
#ifndef NOFIFO // 周期的にUSBUARTの送受信を監視する CY_ISR(int_uartQueue_isr) { uartTxIsr(); } #endif // !define(NOFIFO)
FIFO を使う場合に使用される割り込み処理ルーチンが定義されます。 この中から送信キューの処理ルーチンを呼び出します。
int main(void) { uint32 nLine = 0; // 行番号 CyGlobalIntEnable; // 割り込みの有効化 USBUART_Start(0, USBUART_5V_OPERATION); // 動作電圧5VにてUSBFSコンポーネントを初期化 #ifndef NOFIFO int_uartQueue_StartEx(int_uartQueue_isr); // 周期タイマを起動する #endif // !define(NOFIFO) for(;;) { // 初期化終了まで待機 while (USBUART_GetConfiguration() == 0); USBUART_IsConfigurationChanged(); // CHANGEフラグを確実にクリアする USBUART_CDC_Init(); // CDC機能を起動する for (;;) { // 設定が変更されたら、再初期化をおこなう if (USBUART_IsConfigurationChanged()) { break; } // CDC-IN : ホストにメッセージを送る putdec32(nLine++, 7); putstr(" - HELLO WORLD HELLO WORLD HELLO WORLD HELLO WORLD\n"); // CDC-Control : 制御コマンドは無視する (void)USBUART_IsLineChanged(); } } }
メインループでは、これまでと同様に USB 特有の処理を行います。 アプリケーションとして行っているのは、全体で 59バイトの "HELLO WORLD" 文字列を永遠に出力し続ける処理です。 このとき、先頭に行番号を追加しているので、何番目の出力なのかがわかるようになっています。
実行してみたら
プロジェクトが出来たら、実行してみます。 PSoC 5LP を USB ケーブルを介して PC に接続し、ターミナルソフトを接続すると、このスクリーンショットのように、文字列が延々と表示されます。
一行表示するごとに59バイトのデータが送信されていることになります。 10万行の送信を行った時に必要な時間を測定したところ、43秒かかりました。 ここから、実効スループットは 134kiB/s と計算されました。
周期割り込みの周期を短くすると、スループットは上がりました。 送信するデータの量にしたがって、割り込み周期を決めてやるとよいでしょう。
FIFO を使わなかったら
マクロ NOFIFO を使って、 FIFO を使わない場合の動作も確認しました。 すると、表示が乱れて使い物になりません。 どこに問題が有るのか究明はしていませんが、スループットが高い場合には FIFO を入れないと話にならないという事がわかりました。
プロジェクトアーカイブ
この記事で作成したプロジェクトは、このファイルの拡張子を "zip" に変更すると再現できます。
関連商品
CY8CKIT-059 PSoC 5LP Prototyping Kit
- 出版社/メーカー: スイッチサイエンス
- メディア: エレクトロニクス
SparkFun FreeSoC2 開発ボード - PSoC5LP
- 出版社/メーカー: Sparkfun
- メディア: エレクトロニクス