
最近、
USB大容量記憶装置として、またはUSBフラッシュドライブとしてロシア語で
、デバイスをSTM32F103マイクロコントローラーに接続することをいじっていました。 すべてが比較的単純なようです:グラフィカルコンフィギュレータSTM32CubeMXでは、2、3回クリックするだけでコードが生成され、SDカードドライバーが追加され、出来上がりました。すべてが機能します。 非常にゆっくり-フルスピードモードでのUSBバスの帯域幅がはるかに高いという事実にもかかわらず、200 kB / s-12 mB / s(約1.2 MB / s)。 さらに、オペレーティングシステムでのフラッシュドライブの開始時間は約50秒であり、これは単に仕事上不快です。 私はこのエリアに飛び込んだので、伝送速度を修正してみませんか。
実際、
SDカード用のドライバー (より正確にはSPIドライバー)
を既に作成しました 。これはDMAを介して動作し、最大500 kb / sの速度を提供しました。 残念ながら、USBのコンテキストでは、このドライバーは機能しませんでした。 この理由は、USB通信モデル自体です。すべてが割り込みで行われ、ドライバーは通常のストリームで動作するように調整されています。 はい、粉末化されたプリミティブの同期FreeRTOS。
この記事では、SPIを介してSTM32F103マイクロコントローラーに接続された多数のUSBカードとSDカードを最大限に活用できるように、いくつかのフェイントを作成しました。 また、FreeRTOS、同期オブジェクト、およびDMAを介したデータ転送の一般的なアプローチについても説明します。 ですから、この記事はSTM32コントローラーやDMAなどのツールに精通している人、およびFreeRTOSで作業する際のアプローチに役立つと思います。 このコードは、
STM32CubeパッケージのHALおよびUSBミドルウェアライブラリ、およびSDカードを
操作するための
SdFatに基づいています。
アーキテクチャの概要
個々のコンポーネントの詳細に触れない場合、マイクロコントローラーの側の大容量記憶装置(別名、大容量記憶クラス-MSC)の実装は比較的簡単です。

一方にはUSBコアライブラリがあります。 彼女はホストとの通信に従事しており、デバイスの登録を提供し、あらゆる種類の低レベルのUSBを実装しています。
(USBカーネルを使用する)大容量記憶装置ドライバーは、ホストとの間でデータを送受信できます。 COMポートと同様に、データのみがブロックで送信されます。 ここでは、このデータのセマンティックコンテンツが重要です。SCSIコマンドとデータが送信されます。 さらに、実行するコマンドは数種類しかありません。データの読み取り、データの書き込み、ストレージデバイスのサイズの確認、デバイスの準備状況の確認です。
MSCドライバーのタスクは、SCSIコマンドを解釈し、ストレージドライバーに呼び出しをリダイレクトすることです。 ブロックアクセスが可能な任意のストレージデバイス(RAMディスク、フラッシュドライブ、ネットワークストレージ、CDなど)を使用できます。 私の場合、ストレージデバイスはSPIを介して接続されたmicroSDカードです。 ドライバーに必要な一連の機能はほぼ同じです:読み取り、書き込み、サイズの指定、準備完了状態。
そして、ここでは、1つの重要なニュアンスが表示されます。 実際のところ、USBプロトコルはホスト指向です。 ホストのみがトランザクションを開始し、データを送受信できます。 マイクロコントローラーの観点から見ると、これはUSBに関連するすべてのアクティビティが割り込みのコンテキストで行われることを意味します。 この場合、対応するハンドラーがMSCドライバーで呼び出されます。
マイクロコントローラーからホストにデータを送信する場合。 マイクロコントローラは、それ自体でデータ転送を開始できません。 マイクロコントローラーがUSBコアに通知できる最大値は、ホストが取得できるデータがあることです。
SDカード自体についても、それほど単純ではありません。 実際、カードは複雑なデバイスであり(明らかに独自のマイクロコントローラーを備えている)、通信プロトコルは非常に重要です。 すなわち 特定のアドレスにデータを送信/受信するだけではありません(何らかのI2C EEPROMモジュールの場合のように)。 カードと通信するためのプロトコルは、さまざまなコマンドと確認、チェックサム、およびタイムアウトのセット全体を提供します。
私は
SdFatライブラリを使用し
ています 。 私のデバイスで積極的に使用しているFATファイルシステムレベルのSDカードでの作業を実装しています。 USB接続の場合、ファイルシステムに接続されているものはすべて切断されます(この役割はホストに転送されます)。 しかし、重要なことは、ライブラリは、MSCドライバーが望むものとほぼ同じインターフェース(読み取り、書き込み、サイズの確認)を持つカードドライバーを個別に割り当てます。

カードドライバは、SPIを介してカードと通信するためのプロトコルを実装します。 彼は、どのコマンドをマップに送信するか、どの順序で、どのコマンドを待つのかを正確に知っています。 しかし、ドライバー自体は鉄を扱いません。 このために、もう1つの抽象化レベルが提供されます-個々のブロックの読み取り/書き込み要求をSPIバスを介した実際のデータ転送に変換するSPIドライバー。 この場所でDMAを介したデータ転送を整理できたため、通常モードでのデータ転送速度は向上しましたが、USBの場合はすべてのラズベリーが壊れました(最終的にDMAを無効にする必要がありました)
しかし、まず最初に。
どのような問題を解決しますか?
この質問は同僚からよく聞かれ、技術的な論争中に対談者を困惑させます。このキッチンにはすべて2つの問題があります。
- USBで作業する場合の低直線速度。 主に同期読み取り/書き込み操作によるもの
- 高プロセッサ負荷(最大100%)-デバイスは使用できなくなります。 その理由は、DMAが無効になっており、プロセッサを使用してデータを駆動する必要があるためです。
しかし、これはコントローラー側からのものであり、USB Mass Storageプロトコルにはまだ側面があります。 USB Wiresharkスニファーをインストールし、バス上で実行されているパケットを正確に調べたところ、低速の原因が少なくとも3つあることがわかりました。
- ホストが送信するトランザクションが多すぎる
- 時間とともに引き伸ばされたトランザクション
- 読み取り/書き込み操作自体は同期的に発生し、終了を待機します
トランザクション数の問題は非常に簡単に解決できます。 デバイスを接続すると、OSが
FATテーブル全体を読み取り、ディレクトリとMBRのさまざまな小さな読み取りを行うことが判明しました。 クラスターサイズが4kbのFAT32でフォーマットされた8ギガのフラッシュドライブがあります。 FATテーブルには約8 MB必要です。 200 kb / sの線形伝送速度では、ほぼ40秒になります。
デバイスを接続するときに読み取り操作の数を減らす最も簡単な方法は、FATテーブルを減らすことです。 USBフラッシュドライブを再フォーマットし、クラスターサイズを増やすだけで十分です(これにより、その数とテーブルサイズが小さくなります)。 クラスタサイズを16kに設定してカードをフォーマットしました-FATテーブルサイズは2 MBよりわずかに小さくなり、初期化時間は20秒に短縮されました。
常により良いとは限らない私のデバイスでは、8ギガフラッシュドライブが多すぎるため、それほど必要ないことに気付きました。 1ギガバイト、または512メガバイトで十分です。 そのようなフラッシュドライブはまだ手元にありません。 また、現在でも販売されていません。 ジンバルを削り取らなければなりません。 私が見つけたように、私はしようとします。
いずれにせよ、フラッシュドライブを再フォーマットしても、線形速度の問題(大きなファイルが順次読み取られる速度)は解決されません。 それでも200 kb / sのままで、プロセッサをほとんどロードしません。 それについて何ができるか見てみましょう。
USB DMAの何が問題になっていますか?
最後に、コードに移り、フラッシュカードでの読み取り/書き込みの配置方法を確認します(SPIドライバー)
私のプロジェクトでは、FreeRTOSを使用しています。 これは、デバイスの各機能を個別のスレッド(タスク)で処理できる素晴らしいツールです。 私はなんとか巨大なステートマシンを投げることができ、コードははるかにシンプルで理解しやすくなりました。 すべてのタスクは同時に機能し、互いに譲歩し、必要に応じて同期します。 さて、すべてのスレッドが何らかのイベントを予期してスリープ状態になった場合、マイクロコントローラーの省電力モードを使用できます。
SDカードで機能するコードは、別のスレッドでも機能します。 これにより、読み取り/書き込み機能を非常にエレガントに作成できました。
DMAを使用してSDカードにデータを読み書きするためのSPIドライバーuint8_t SdFatSPIDriver::receive(uint8_t* buf, size_t n) {
ここでの全体の魅力は、大きなデータブロックを読み書きする必要があるとき、このコードが完了を待たないことです。 代わりに、DMAを介したデータ転送が開始され、ストリームがスリープ状態になります。 この場合、プロセッサーはビジネスに取り掛かることができ、制御の転送は他のスレッドに移ります。 転送が完了すると、DMAからの割り込みが呼び出され、データの転送を待機していたストリームが起動します。
誰がここにいますか?このようなトリックが初めての場合は、まず
ここまたは
ここで 、シグナル待機モードで動作するセマフォの原理を理解することをお勧めします
問題は、作業のすべてのロジックが通常の実行スレッドではなく、割り込みで発生するUSBモデルでは、このようなアプローチが難しいことです。 すなわち 割り込みで読み取り/書き込み要求を受信し、データ転送の完了も同じ割り込みで待機する必要があります。
もちろん、中断のコンテキストでDMAを介した転送を手配することもできますが、これにはほとんど意味がありません。 DMAは、転送を開始し、データ転送が終了するまでプロセッサを他の有用な作業に切り替えることができる場所でうまく機能します。 しかし、割り込みから転送を開始すると、割り込みを中断することはできなくなり(トートロジーについてはごめんなさい)、ビジネスを続けることができなくなります。 そこにハングアップし、転送の終了を待つ必要があります。 すなわち 操作は同期され、合計時間はDMAなしの場合と同じになります。
ここで、ホストの要求に応じて、DMAを介してデータの送信を開始し、割り込みを終了する方がはるかに興味深いでしょう。 そして、どういうわけか、行われた作業に関する次の中断レポートで。
しかし、これは全体像ではありません。 カードからの読み取りがデータブロックの送信のみで構成されている場合、このアプローチの実装は難しくありません。 しかし、SPI伝送は最も重要な部分ですが、それだけではありません。 マップドライバーのレベルでデータブロックの読み取り/書き込みを見ると、プロセスは次のようになります。
- カードにコマンドを送信し、待機して応答を確認します
- カードの準備ができるまで待ちます
- データを転送します(ここで上に引用したのと同じ関数があります)
- チェックサムを計算し、カードの意見と比較します
- 完全な転送
この一見線形のアルゴリズムが一連のネストされた関数呼び出しによって実装されていることを考えると、途中でカットすることはあまり合理的ではありません。 ライブラリ全体を適切に粉砕する必要があります。 場合によっては、転送を1つのピースではなく、一連の小さなブロックを使用したサイクルで実行できることを考慮すると、タスクは完全に不可能になります。
しかし、すべてがそれほど悪いわけではありません。 MSCドライバーのレベルでさらに高く見ると、DMAを使用して、または使用せずに、1ブロックまたは複数ブロックのデータがどの程度正確に送信されるかが一般的にわかります。 主なことは、データを送信し、ステータスを報告することです。
実験する理想的な場所は、MSCドライバーとカードドライバーの間のレイヤーです。 いじめの前は、このコンポーネントは非常に簡単に見えました。実際、MSCドライバーを確認したいインターフェイスとカードドライバーが提供するものとの間のアダプターです。
オリジナルのアダプター実装 int8_t SD_MSC_Read (uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { (void)lun;
既に述べたように、割り込みの下から呼び出された場合、カードドライバーは機能しません。 しかし、通常のスレッドではうまく機能します。 それでは、別のスレッドを開始しましょう。
このスレッドは、キューを介して読み取りおよび書き込み要求を受け取ります。 各要求には、操作の種類(読み取り/書き込み)、読み取りまたは書き込みを行うブロックの数、ブロックの数、およびデータバッファーへのポインターに関する情報が含まれます。 また、操作のコンテキストへのポインタも取得しました。少し後で必要になります。
読み取り/書き込み要求キュー enum IOOperation { IO_Read, IO_Write }; struct IOMsg { IOOperation op; uint32_t lba; uint8_t * buf; uint16_t len; void * context; };
スレッド自体はコマンドを待ってスリープしています。 コマンドが到着すると、目的の操作がさらに同期的に実行されます。 操作の最後にコールバックを呼び出します。コールバックは、実装に応じて、読み取り/書き込み操作の最後に必要な処理を行います。
カードへの読み取り/書き込みを提供するストリーム extern "C" void cardReadCompletedCB(uint8_t res, void * context); extern "C" void cardWriteCompletedCB(uint8_t res, void * context); void xSDIOThread(void *pvParameters) { while(true) { IOMsg msg; if(xQueueReceive(sdCmdQueue, &msg, portMAX_DELAY)) { switch(msg.op) { case IO_Read: { bool res = card.readBlocks(msg.lba, msg.buf, msg.len); cardReadCompletedCB(res ? 0 : 0xff, msg.context); break; } case IO_Write: { bool res = card.writeBlocks(msg.lba, msg.buf, msg.len); cardWriteCompletedCB(res? 0 : 0xff, msg.context); break; } default: break; } } } }
これらはすべて通常のフローの一部として実行されるため、内部のカードドライバーはDMAとFreeRTOSの同期を使用できます。
MSC関数はもう少し複雑になりましたが、それほど複雑ではありません。 現在、このコードは直接読み書きする代わりに、対応するストリームにリクエストを送信します。
読み取り/書き込み要求の送信 int8_t SD_MSC_Read (uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len, void * context) {
重要なポイントがあります-これらの関数のセマンティクスが変更されました。 現在、それらは非同期、つまり 操作の実際の終了を待たないでください。 そのため、それらを呼び出すコードを微調整する必要がありますが、これについては少し後で説明します。
それまでの間、これらの関数をテストするために、別のテストスレッドを作成します。 USBコアをエミュレートし、読み取り要求を送信します。
テストストリーム uint8_t io_buf[1024]; static TaskHandle_t xTestTask = NULL; void cardReadCompletedCB(bool res, void * context) { xTaskNotifyGive(xTestTask); } void cardWriteCompletedCB(bool res, void * context) { xTaskNotifyGive(xTestTask); } void xSDTestThread(void *pvParameters) { xTestTask = xTaskGetCurrentTaskHandle(); uint32_t prev = HAL_GetTick(); uint32_t opsPer1s = 0; uint32_t cardSize = card.cardSize(); for(uint32_t i=0; i<cardSize; i++) { opsPer1s++; if(SD_MSC_Read(0, io_buf, i, 2, NULL) != 0) usbDebugWrite("Failed to read block %d\r\n", i); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); if(HAL_GetTick() - prev > 1000) { prev = HAL_GetTick(); usbDebugWrite("Reading speed: %d kbytes/s\r\n", opsPer1s); opsPer1s = 0; } } while(true) ; }
このコードは、1kbのブロックでカード全体を最初から最後まで読み取り、読み取り速度を測定します。 各読み取り操作は、要求をSDカードストリームに送信します。 そこで、同期的に読み取りが行われ、コールバックを介して終了が報告されます。 このコールバックの実装を置き換えました。このコールバックは、テストスレッドに続行できることを通知するだけです(テストスレッドは、ulTaskNotifyTake()関数で常にスリープ状態になっています)。
しかし、最も重要なことは、このバージョンの読み取り速度は約450kb / sであり、プロセッサの負荷は3〜4%に過ぎないことです。 私の意見では、悪くない。
ドライバーMSCをポンプします
そこで、DMAを有効にしてカードドライバーを獲得しました。 ただし、読み取り/書き込みのセマンティクスは同期から非同期に変更されています。 次に、MSCの実装を微調整して、非同期呼び出しの操作方法を教える必要があります。 すなわち DMAを介したホストからの最初の要求への転送を開始し、「前の操作はまだ終了していないので、後で確認してください」と言って、後続のすべての要求に何らかの方法で応答する必要があります
実際、
USBプロトコルはそのようなメカニズムをすぐに使用できます 。 受信側は、特定のステータスでデータの転送を確認します。 データが正常に受信および処理されると、受信者はACKステータスでトランザクションを確認します。 デバイスがトランザクションを処理できない場合(初期化されていない、エラー状態にある、またはその他の理由で機能しない)、応答はステータスSTALLになります。
しかし、デバイスがトランザクションを認識し、健全な状態にあるが、データの準備がまだ整っていない場合、デバイスはNAKに応答できます。 この場合、ホストは少し遅れてまったく同じリクエストでデバイスに接続する必要があります。 このステータスを遅延読み取り/書き込みに使用できます。ホストへの最初の呼び出しで、DMAを介してデータの送信を開始しますが、トランザクションNAKに応答します。 ホストが繰り返しトランザクションを開始し、DMAを介した転送がすでに終了している場合、ACKで応答します。
残念ながら、STからUSBライブラリでNAK信号を送信する良い方法が見つかりませんでした。 関数の戻りコードはチェックされないか、2つの状態しか処理できません-すべてが正常であるか、エラーです。 2番目の場合、すべてのエンドポイントが閉じられ、ステータスSTALLがすべての場所に設定されます。
USBドライバーの最下位レベルでは、NAK確認が非常に積極的に使用されていると思われますが、クラスドライバーレベルでNAKを適切に使用する方法はわかりませんでした。
どうやら、STのライブラリの作成者は、さまざまな確認の代わりに、より人道的なインターフェースを提供しました。 デバイスがホストに送信するものを持っている場合、USBD_LL_Transmit()関数を呼び出します-ホスト自体が提供されたデータを取得します。 関数が呼び出されなかった場合、デバイスは自動的にNAK応答で応答します。 データの受信とほぼ同じ状況。 デバイスの受信準備ができている場合、USBD_LL_PrepareReceive()関数を呼び出します。 それ以外の場合、ホストがデータを送信しようとすると、デバイスはNAKに応答します。 この知識を使用して、MSCドライバーを実装します。
USBバスで実行されるトランザクションを見てみましょう(分析は、カードドライバーの変更前に実行されました)。

ここで興味深いのはトランザクション自体ではなく、それらのタイムスタンプです。 この図のトランザクションは、「ライト」-処理を必要としないトランザクションを選択しました。 マイクロコントローラは、そのようなリクエストにハードコードされた応答で、あまり考えずに応答します。 ここで重要なことは、ホストがトランザクションを連続ストリームでフラッディングしないことです。 トランザクションは1ミリ秒ごとに1回しか実行されません。 回答がすぐに準備できたとしても、ホストは次のトランザクションでのみ1msで回答を取得します。
これは、USBバス上のトランザクションに関して、1ブロックのデータを読み取る方法のようです。

最初に、ホストはSCSI読み取りコマンドを送信し、次に別のトランザクションでデータ(2行目)とステータス(3行目)を読み取ります。 最初のトランザクションは最長です。 このトランザクションの処理中、マイクロコントローラーはカードからの控除に関与します。 また、トランザクション間で、ホストは1ミリ秒間停止します。
ところで。USBの用語では、ホストからデバイスへの方向はOUTと呼ばれますが、コントローラーの場合、これは手法です。 逆に、デバイスからホストへの方向はINと呼ばれますが、私たちにとってこれはデータの送信を意味します。
マイクロコントローラー側のMSCドライバーアルゴリズムは次のようになります
- SCSIトランザクション:読み取り(10)LUN:0x00(LBA:0x00000000、Len:1)
- ホストは読み取りコマンドを送信します。 マイクロコントローラー側では、MSC_BOT_DataOut()関数が呼び出されます
- コマンドは、一連の関数MSC_BOT_DataOut()-> MSC_BOT_CBW_Decode()-> SCSI_ProcessCmd()-> SCSI_Read10()によって処理されます。
- ドライバーはhmsc-> bot_state == USBD_BOT_IDLE状態にあるため、読み取り手順が準備されています。コマンドパラメーターがチェックされ、読み取りが必要な合計ブロック数が確認され、最初のブロックの読み取り要求とともにSCSI_ProcessRead()関数に制御が転送されます
- SCSI_ProcessRead()関数は、データを同期的に読み取ります。 これは、ほとんどの場合、マイクロコントローラが忙しい場所です。
- データが受信されると、(USBD_LL_Transmit()関数を使用して)データがエンドポイントMSC_INの出力バッファーに転送され、ホストがそれを取得できるようになります。
- ドライバーはhmsc-> bot_state = USBD_BOT_DATA_IN状態になります
- SCSIトランザクション:データ入力
- ホストは、マイクロコントローラーの出力バッファーから64バイトのパケットでデータを収集します(USBフルスピードデバイスの最大推奨パケットサイズ)。 これらはすべてUSBコアの最下位レベルで発生し、MSCドライバーは関与しません
- ホストがすべてのデータを取得すると、Data Inイベントが発生します。 制御はMSC_BOT_DataIn()関数に渡されます。 この関数は、実際のデータ送信後に呼び出されることを強調します。
- ドライバーの状態はhmsc-> bot_state == USBD_BOT_DATA_INです。これは、読み取りモードのままであることを意味します。
- すべての順序付けられたブロックがまだ読み取られていない場合、次のピースの読み取りを開始し、完了を待機し、出力バッファーにシフトして、ホストがデータを取得するまで待機します。 アルゴリズムの繰り返し
- すべてのブロックが読み取られると、ドライバーはUSBD_BOT_LAST_DATA_INに切り替わり、コマンドの最終ステータスを送信します
- SCSIトランザクション:応答
- この時点で、区画データはすでに送信されています
- ドライバーはこれに関する通知のみを受け取り、USBD_BOT_IDLE状態になります
このスキームで最も長い操作は、実際にはカードからの読み取りです。 私の測定によると、同期モードでの読み取りには約2〜3msかかります。 さらに、転送はプロセッサーによって行われ、これはすべて割り込みUSBで行われます。 比較のために、DMAを介して長さ512の1ブロックを減算するには、1ミリ秒以上かかります。
データの読み取りを大幅に(たとえば1Mb / sまで)スピードアップできませんでした-これは、SPIを介して接続されたカードの帯域幅です。 しかし、トランザクションの間に1ミリ秒の休止をサービスに置くことを試みることができます。
このように見えます(わずかに簡略化されています)
- SCSIトランザクション:読み取り(10)LUN:0x00(LBA:0x00000000、Len:1)
- マイクロコントローラーは読み取りコマンドを受信し、すべてのパラメーターをチェックし、読み取りが必要なブロックの数を記憶します
- マイクロコントローラーは、非同期モードで最初のブロックの読み取りを開始します
- 読み取りの終了を待たずに割り込みを終了します
- 読み取りが終了すると、コールバックが呼び出されます
- 読み取りデータは出力バッファーに送信されます。
- ホストはMSCドライバーなしでそれらを読み取ります
- SCSIトランザクション:データ入力
- コールバック関数DataIn()が呼び出されます。これは、ホストがデータを取得し、次の読み取りを実行できることを通知します
- 次のブロックの読み取りを開始します。 アルゴリズムは読み取り完了コールバックから繰り返します
- すべてのブロックが読み取られた場合、ステータスパケットを送信します
- SCSIトランザクション:応答
- この時点で、区画データはすでに送信されています
- 次のトランザクションの準備をしています。
SCSI_ProcessRead()関数は「前」と「後」に簡単に分割されるため、このアプローチを実装してみましょう。 つまり、読み取りを開始するコードは割り込みのコンテキストで実行され、残りのコードはコールバックに移動します。 このコールバックのタスクは、読み取られたデータを出力バッファーにプッシュすることです(ホストは何らかの方法で対応する要求でこのデータを取得します)
非同期読み取りに適合したSCSI_ProcessRead()関数 static int8_t SCSI_ProcessRead (USBD_HandleTypeDef *pdev, uint8_t lun) { USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC; uint32_t len; len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET); if( pdev->pClassSpecificInterfaceMSC->Read(lun , hmsc->bot_data, hmsc->scsi_blk_addr / hmsc->scsi_blk_size, len / hmsc->scsi_blk_size, pdev) < 0) { SCSI_SenseCode(pdev, lun, HARDWARE_ERROR, UNRECOVERED_READ_ERROR); return -1; } hmsc->bot_state = USBD_BOT_DATA_IN; return 0; } void cardReadCompletedCB(uint8_t res, void * context) { USBD_HandleTypeDef * pdev = (USBD_HandleTypeDef *)context; USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC; uint8_t lun = hmsc->cbw.bLUN; uint32_t len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET); if(res != 0) { SCSI_SenseCode(pdev, lun, HARDWARE_ERROR, UNRECOVERED_READ_ERROR); return; } USBD_LL_Transmit (pdev, MSC_IN_EP, hmsc->bot_data, len); hmsc->scsi_blk_addr += len; hmsc->scsi_blk_len -= len; hmsc->csw.dDataResidue -= len; if (hmsc->scsi_blk_len == 0) { hmsc->bot_state = USBD_BOT_LAST_DATA_IN; } }
コールバックでは、SCSI_ProcessRead()関数で定義されているいくつかの変数(USBハンドルへのポインター、送信されたブロックの長さ、LUN)にアクセスする必要があります。 これは、コンテキストパラメータが便利な場所です。 確かに、私はすべてを送信したのではなく、pdevのみを送信しました。 私にとっては、このアプローチは、構造全体を目的のフィールドでプルするよりも簡単です。 そして、いずれにせよ、これはいくつかのグローバル変数を持つよりも優れています。
ダブルバッファーを追加する
一般に、このアプローチは機能しましたが、速度は依然として200kb / sを少し上回りました(ただし、プロセッサの負荷は修復され、約2〜3%になりました)。 私たちがより速く働くことを妨げるものを理解しましょう。
私の記事の1つに対するコメントのアドバイスで、私はまだオシロスコープを入手しました(安価なものですが)。 彼はそこで何が起こっているのかを理解するのに非常に役立ちました。 未使用のピンを取り、読み取り前に1に設定し、読み取り終了後にゼロに設定しました。 オシロスコープでは、読み取りプロセスは次のようになりました。

すなわち 512バイトの読み取りには1ms少しかかります。 カードからの読み取りが終了すると、データは出力バッファーに転送され、ホストは次の1ミリ秒でデータを取得します。 すなわち ここでは、カードから読み取るかUSB経由で転送しますが、同時にではありません。
通常、この状況はダブルバッファリングによって解決されます。 さらに、STM32F103マイクロコントローラUSB周辺機器は、すでにデュアルバッファリングメカニズムを提供しています。 次の2つの理由で、それらだけが私たちに合わないでしょう。
- マイクロコントローラ自体が提供するダブルバッファリングを使用するには、USBコアとMSC実装を再描画する必要がある場合があります
- バッファサイズは64バイトのみですが、SDカードは512バイト未満のブロックでは機能しません。
そのため、実装を考案する必要があります。 ただし、これは難しくありません。 最初に、2番目のバッファー用の場所を予約します。 私は彼のために別の変数を開始しませんでしたが、単に既存のバッファーを2倍に増やしました。 また、変数bot_data_idxを作成する必要がありました。これは、このダブルバッファーのどの半分が現在使用されているかを示します。0-前半、1-2番目。
ダブルバッファー typedef struct _USBD_MSC_BOT_HandleTypeDef { ... USBD_MSC_BOT_CBWTypeDef cbw; USBD_MSC_BOT_CSWTypeDef csw; uint16_t bot_data_length; uint8_t bot_data[2 * MSC_MEDIA_PACKET]; uint8_t bot_data_idx; ... } USBD_MSC_BOT_HandleTypeDef;
ところで、cbw構造とcsw構造はアライメントに非常に敏感です。 一部の値は、これらの構造体のフィールドに対して誤って書き込まれたり読み取られたりしました。 そのため、データバッファよりも高く転送する必要がありました。
元の実装は、DataIn割り込み(データが送信されたというシグナル)で機能していました。 すなわち ホストからのコマンドにより、読み取りが開始され、その後データが出力バッファーに転送されました。 データの次のバッチの読み取りは、DataInを中断することで「リチャージ」されました。 このオプションは適切ではありません。 前回の読み取りが終了した直後に読み取りを開始します。
前の読み取りが終了した直後に読み取り値を充電する void cardReadCompletedCB(uint8_t res, void * context) { USBD_HandleTypeDef * pdev = (USBD_HandleTypeDef *)context; USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC; uint8_t lun = hmsc->cbw.bLUN; uint32_t len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET); if(res != 0) { SCSI_SenseCode(pdev, lun, HARDWARE_ERROR, UNRECOVERED_READ_ERROR); return; }
この関数は、構造をわずかに変更しました。まず、ここでダブルバッファリングが実装されます。この関数はカードからの読み取りが終了すると呼び出されるため、SCSI_ProcessRead()を呼び出すことで、すぐに次の読み取りを開始できます。新しい読み取り値が読み取られたばかりのデータを消去しないように、2番目のバッファーを使用します。変数bot_data_idxは、バッファーの切り替えを担当します。しかし、それだけではありません。
第二に、アクションのシーケンスが変更されました。現在、次のデータブロックの読み取りが最初に課金され、USBD_LL_Transmit()が呼び出されます。これは、cardReadCompletedCB()関数が通常のスレッドのコンテキストで呼び出されるためです。最初にUSBD_LL_Transmit()を呼び出してからhmscフィールドの値を変更すると、現時点でUSBからの割り込みが発生する可能性があり、これらのフィールドも変更する必要があります。第三に、追加の同期を強化する必要がありました。実際には、通常、カードからの読み取りにはUSB経由の転送よりも少し時間がかかります。ただし、逆の場合もあり、次のブロックのUSBD_LL_Transmit()呼び出しは、前のブロックが完全に送信される前に発生します。 USBコアはこのような厚かましさからだまされ、データは正しく送信されません。
データの送信(送信)はデータ入力イベントによって確認されますが、複数の送信が連続して発生する場合があります。このような場合、同期が必要です。これは、少しの同期を追加するだけで非常に簡単に解決されます。 USBD_StorageTypeDefインターフェイスに、かなり単純な実装の関数をいくつか追加しました(ただし、おそらく名前はあまり成功していません)。実装は、シグナル待機モードで通常のセマフォを使用します。コールバックで呼び出されるOnFinishOp()cardReadCompletedCB()はスリープし、前のデータパケットが送信されるまで待機します。送信の事実はDataInイベントによって確認されます。このイベントはSCSI_Read10()関数によって処理され、OnStartOp()を呼び出します。OnStartOp()はOnFinishOp()をロック解除し、次のデータパケットを。関数が逆の順序で呼び出されたとしても(そしてこれが最初の読み取り中に起こることとまったく同じです-最初のSCSI_Read10()、次にcardReadCompletedCB())、すべてが正常に機能します(シグナル待機モードのセマフォプロパティ)。同期機能を実装する void SD_MSC_OnStartOp() { xSemaphoreGiveFromISR(usbTransmitSema, NULL); } void SD_MSC_OnFinishOp() { xSemaphoreTake(usbTransmitSema, portMAX_DELAY); }
このような同期により、画像は次の形式を取ります。
赤い矢印は同期を示します。最後の送信は、前のデータ入力を待っています。パズルの最後のピースは、SCSI_Read10()関数です。関数SCSI_Read10()、データ入力イベントによって呼び出されます static int8_t SCSI_Read10(USBD_HandleTypeDef *pdev, uint8_t lun , uint8_t *params) { USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC;
SCSI_Read10()の元の実装では、関数への最初の呼び出しでパラメーターがチェックされ、最初のブロックの読み取りプロセスが開始されました。前のパケットが既に送信されていて、次のパケットの読み取りを開始する必要があるときにDataInが中断されると、後で同じ関数が呼び出されます。両方のブランチは、SCSI_ProcessRead()関数を使用して読み取りを開始しました。新しい実装では、SCSI_ProcessRead()の呼び出しはif内に移動し、最初のブロック(bot_state == USBD_BOT_IDLE)を読み取るためだけに呼び出され、後続のブロックの読み取りはcardReadCompletedCB()から開始されます。その結果を見てみましょう。オシロスコープでそのようなノッチを見るために、ブロックの読み取りの間に意図的にわずかな遅延を追加しました。実際、読み取り操作の間隔が非常に短いため、オシロスコープにはこれが表示されません。
この写真からわかるように、アイデアは成功でした。前の操作が終了するとすぐに、新しい読み取り操作が開始されます。読み取り間の一時停止は非常に小さく、主にホストによって決定されます(トランザクション間の1ミリ秒の同じ遅延)。大きなファイルの平均読み取り速度は400〜440kb / sに達し、非常に良好です。最後に、プロセッサの負荷は約2%です。しかし、記録はどうですか?
カードに記録するという話題を巧みに避けながら。しかし、現在では、MSCドライバーの知識と理解があれば、記録機能の実装を複雑にする必要はありません。元の実装は次のように動作します。- SCSI書き込みトランザクション
- コマンドは、一連の関数MSC_BOT_DataOut()-> MSC_BOT_CBW_Decode()-> SCSI_ProcessCmd()-> SCSI_Write10()によって処理されます。
- hmsc->bot_state == USBD_BOT_IDLE, : ,
- USBD_LL_PrepareReceive() USB .
- hmsc->bot_state = USBD_BOT_DATA_OUT
- SCSI: Data Out
- 64 . USB, MSC
- Data Out SCSI_Write10()
- hmsc->bot_state == USBD_BOT_DATA_OUT, SCSI_ProcessWrite()
- 同期モードでカードに録音があります
- すべてのデータがまだ受信されていない場合は、USBD_LL_PrepareReceive()を呼び出して受信を「再充電」します
- すべてのブロックが書き込まれると、関数MSC_BOT_SendCSW()が呼び出され、ホストに確認が送信され(制御ステータスワード-CSW)、ドライバーはUSBD_BOT_IDLEに切り替わります。
- SCSIトランザクション:応答
- この時点で、ステータスパッケージは既に送信されています。対処不要
最初に、元の実装をWrite()関数の非同期に適合させます。SCSI_ProcessWrite()関数を分離し、コールバックで残りの半分を呼び出すだけです。レコード機能の実装 static int8_t SCSI_ProcessWrite (USBD_HandleTypeDef *pdev, uint8_t lun) { uint32_t len; USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC; len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET); if(pdev->pClassSpecificInterfaceMSC->Write(lun , hmsc->bot_data, hmsc->scsi_blk_addr / hmsc->scsi_blk_size, len / hmsc->scsi_blk_size, pdev) < 0) { SCSI_SenseCode(pdev, lun, HARDWARE_ERROR, WRITE_FAULT); return -1; } return 0; } return 0; } void cardWriteCompletedCB(uint8_t res, void * context) { USBD_HandleTypeDef * pdev = (USBD_HandleTypeDef *)context; USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC; uint8_t lun = hmsc->cbw.bLUN; uint32_t len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET);
読み取りの場合と同様に、最初の関数から2番目の関数に何らかの方法で変数を渡す必要があります。このために、コンテキストパラメータを使用してUSBデバイスのハンドルを渡します(そこから必要なデータをすべて取得できます)。このモードでの記録速度は約90kb / sで、主にカードへの書き込み速度によって制限されます。これは波形によって確認されます-各ピークは1ブロックの記録です。画像から判断すると、512バイトの記録には3〜6ミリ秒かかります(毎回異なる方法で)。
さらに、レコードは100msから0.5sに固定される場合があります-明らかにカードのどこかにさまざまな内部アクティビティが必要です-ブロックの再マッピング、ページの消去、またはそのようなもの。このことから進んで、ダブルバッファにドーピングしても状況が劇的に改善することはほとんどありません。しかし、とにかく、私たちは純粋にスポーツの興味からこれをやろうとします。したがって、この演習の本質は、前のブロックがカードに書き込まれている間にホストから次のブロックを取得することです。SCSI_Write10()関数のどこかで記録と次のブロックの受信を同時に開始するオプションがすぐに思い浮かびます。DataOutイベントによって(次のブロックの受信が完了します)。何も機能しません。なぜなら
受信は記録よりもはるかに高速で、カードが書き込みを管理するよりも多くのデータを受信できます。 すなわち
次のデータは以前に受け入れられたが上書きされますが、まだ処理されていません。
このスキームでは、複数のパケットを連続して受信できますが、すべてのパケットがSDカードに記録されるわけではありません。ほとんどの場合、データの一部は次のブロックで消去されます。同期を行う必要があります。どこだけ?読み取り操作の場合、カードからの読み取りが終了し、データがUSBに転送される場所でダブルバッファリングと同期を行いました。この場所はcardReadCompletedCB()関数でした。書き込み操作の場合、SCSI_Write10()関数はそのような中心的な場所になります。次のデータブロックが受信されたときにそこにあり、ここからカードへの書き込みを開始します。ただし、cardReadCompletedCB()関数とSCSI_Write10()関数には基本的な違いが1つあります。1つ目はSDカードストリームで機能し、2つ目はUSB割り込みで機能します。何らかのイベントまたは同期オブジェクトを待機している間、通常のスレッドが中断される場合があります。中断すると、このようなフォーカスは機能しません-FromISRサフィックスを持つすべてのFreeRTOS関数は非ブロッキングです。これらは、必要に応じて機能します(空きがある場合はリソースをキャプチャし、スペースまたは必要なメッセージがある場合はキューを介してメッセージを送受信します)、またはこれらの関数はエラーを返します。しかし、彼らは決して待ちません。ただし、割り込みで待機を編成することが不可能な場合は、割り込みが再び呼び出されないようにすることができます。より正確には、これでさえ、中断が必要なときに正確に何度も発生するということです。受信/録音プロセス中に発生する可能性のあるいくつかのケースを見てみましょう。ケース番号1:最初のブロックの受信。最初のブロックが受信されるとすぐに、このブロックの記録を開始できます。同時に、2番目のブロックの受信を開始できます。これにより、前のブロックがカードに書き込まれている間に次のブロックを受け入れない場合に一時停止が保存されます。ケース2:トランザクションの途中でブロックを受け取る。ほとんどの場合、両方のバッファがすでにいっぱいになっています。 SDカードストリームのどこかで、最初のブロックからデータブロックが書き込まれ、2番目のブロックはホストから受信したばかりです。原則として、2番目のブロックのレコードを請求することを妨げるものは何もありません-入力にキューがあり(上記のSD_MSC_Read()関数を参照)、入力要求を調整し、ブロックを順番に書き込みます。このキューに2つのリクエストの場所があることを確認する必要があります。しかし、受信を調整する方法は?受信バッファーは2つしかありません。 2番目のブロックを受信した直後に次のブロックの受信を開始すると、最初のバッファーのデータが上書きされ、そこからカードへの記録が現在行われます。この場合、バッファーが解放されたとき、つまり記録が終了したとき(つまり、書き込み関数のコールバックで)に、次のデータブロックの受信を開始する方が適切です。最後に、ケース番号3:受信/録音手順を正しく完了する必要があります。最後のブロックではすべてが明確です。次のブロックを受信する代わりに、データが受信され、トランザクションを閉じることができることをCSWホストに送信する必要があります。ただし、トランザクションの開始時に既に追加のレセプションを編成しているため、最後から2番目のブロックは追加のブロックを注文しないことに注意してください。これらのケースを説明する写真を次に示します。
ケース1:最初のDataOutで、すぐに2番目のブロックの受信を開始します。ケース2:記録が終了し、バッファが解放された後にのみ、次のブロックの受信を開始します。ケース3:最後から2番目の記録で受信を開始せず、最後の記録でCSWを送信します。興味深い観察:カードへの記録が最初のバッファから来た場合、記録の終わりに次のブロックが同じ最初のバッファで受信されます。同様に、2番目のバッファーを使用します。この事実を実装に使用したいと思います。計画を実行してみましょう。最初のケース(追加のブロックを受け取る)を実装するには、特別な状態が必要です最初のブロックを受信するための新しい状態 #define USBD_BOT_DATA_OUT_1ST 6
そしてその処理 void MSC_BOT_DataOut (USBD_HandleTypeDef *pdev, uint8_t epnum) { USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC; switch (hmsc->bot_state) { case USBD_BOT_IDLE: MSC_BOT_CBW_Decode(pdev); break; case USBD_BOT_DATA_OUT: case USBD_BOT_DATA_OUT_1ST: if(SCSI_ProcessCmd(pdev, hmsc->cbw.bLUN, &hmsc->cbw.CB[0]) < 0) { MSC_BOT_SendCSW (pdev, USBD_CSW_CMD_FAILED); } break; default: break; } }
2番目のケース(記録の最後にブロックを受け取る)を実装するには、何らかの形で一定量の情報をコールバックに転送する必要があります。これを行うために、記録コンテキストを持つ構造体を作成し、USBハンドルでこの構造体の2つのインスタンスを宣言しました。レコードコンテキスト typedef struct { uint32_t next_write_len; uint8_t * buf; USBD_HandleTypeDef * pdev; } USBD_WriteBlockContext; typedef struct _USBD_MSC_BOT_HandleTypeDef { … USBD_WriteBlockContext write_ctxt[2]; ... } USBD_MSC_BOT_HandleTypeDef;
SDカードストリームの記録キューのサイズを変更することを忘れないでくださいSCSI_Write10()関数はほとんど変更されていません。ダブルバッファーインデックスの初期化とUSBD_BOT_DATA_OUT_1ST状態への移行のみが追加されています。SCSI_Write10()関数 static int8_t SCSI_Write10 (USBD_HandleTypeDef *pdev, uint8_t lun , uint8_t *params) { USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC; if (hmsc->bot_state == USBD_BOT_IDLE) {
最も興味深いロジックはすべてSCSI_ProcessWrite()関数に集中します-これはバッファーが割り当てられ、読み取りとレコードのチェーン全体が構築される場所です。SCSI_ProcessWrite()関数 static int8_t SCSI_ProcessWrite (USBD_HandleTypeDef *pdev, uint8_t lun) { USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC; uint32_t len = MIN(hmsc->scsi_blk_len , MSC_MEDIA_PACKET); USBD_WriteBlockContext * ctxt = hmsc->write_ctxt + hmsc->bot_data_idx;
まず、ここで記録コンテキストが準備されています-コールバックに送信される情報。特に、このブロックの記録が終了したときの処理を決定します。- 通常の場合、同じバッファへの次のブロックの受信を開始します(上記のケースNo. 2)
- 最後から2番目のブロックの場合、何もしません(ケースNo. 3)
- 最後のブロックの場合、コントロールステータスワード(CSW)-操作のステータスに関するホストへのレポートを送信します
データブロックがカードの書き込みキューに送信された後、バッファーインデックス(bot_data_idx)は代替のインデックスに切り替わります。 すなわち
次のパケットは別のバッファで受信されます。最後に、特別なケース(ケースNo. 1)-最初のブロック(USBD_BOT_DATA_OUT_1ST状態)の場合、追加のデータ受信を編成しますこのコードの応答部分は、カードへの記録完了時のコールバックです。記録されたブロックに応じて、次のブロックの受信が編成され、CSWが送信されるか、何も起こりません。コールバック録音機能 void cardWriteCompletedCB(uint8_t res, void * context) { USBD_WriteBlockContext * ctxt = (USBD_WriteBlockContext*)context; USBD_HandleTypeDef * pdev = ctxt->pdev; USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC; uint8_t lun = hmsc->cbw.bLUN;
最後の和音は同期であり、その本質は写真に表示しやすいです。
非常にまれですが、それでも、次のパケットを受信する前にカードへの書き込みが終了する場合があります。その結果、コードは(同期がなかった場合)別のパケットを要求できますが、現在のパケットはまだ完全に受信されていません。これを防ぐには、同期を追加する必要がありました。これで、次のブロックの受信を要求する前に、コードは前のブロックの受信が終了するまで待機します。読み取り時に使用された同期ツール(OnStartOp()/ OnFinishOp())は非常に適しています。同期する必要がある条件は非常に注意が必要です。トランザクションの開始時に追加のブロックを受信すると、1ブロックのシフトで同期が行われます。したがって、N番目のブロックのコールバックレコードは、N + 1ブロックの受信を待機しています。これは、最初のブロックの受信(USBからの割り込みのコンテキストで発生)と最後の記録(SDカードストリームのコンテキストで発生)が同期を必要としないことを意味します。
赤い矢印が黒い矢印を複製し、次のブロックの記録を開始するように見える場合があります。しかし、コードを見ると、そうではないことがわかります。赤(同期)はMSCドライバーのコードを同期し(青のボックス)、キューはカードドライバー(SDカードストリームのメインループがある)で処理されます。さまざまなコンポーネントのコードに干渉したくありませんでした。少しログを設定すると、4kbのデータレコードは次のようになります4 kbブロック記録の債務ログStarting write operation for LBA=0041C600, len=4096
Receiving first block into buf=1
Writing block of data for LBA=0041C600, len=512, buf=0
This will be regular block
Receiving an extra block into buf=1
Writing block of data for LBA=0041C800, len=512, buf=1
This will be regular block
Write completed callback with status 0 (buf=0)
Preparing next receive into buf=0
Writing block of data for LBA=0041CA00, len=512, buf=0
This will be regular block
Write completed callback with status 0 (buf=1)
Preparing next receive into buf=1
Writing block of data for LBA=0041CC00, len=512, buf=1
This will be regular block
Write completed callback with status 0 (buf=0)
Preparing next receive into buf=0
Writing block of data for LBA=0041CE00, len=512, buf=0
This will be regular block
Write completed callback with status 0 (buf=1)
Preparing next receive into buf=1
Writing block of data for LBA=0041D000, len=512, buf=1
This will be regular block
Write completed callback with status 0 (buf=0)
Preparing next receive into buf=0
Writing block of data for LBA=0041D200, len=512, buf=0
This will be one before the last block
Write completed callback with status 0 (buf=1)
Preparing next receive into buf=1
Writing block of data for LBA=0041D400, len=512, buf=1
This will be the last block
Write completed callback with status 0 (buf=0)
Write completed callback with status 0 (buf=1)
Write finished. Sending CSW
予想どおり、これにより速度が大幅に向上することはありませんでした。変更後、速度は95〜100 kb / sでした。しかし、私が言ったように、それはすべてスポーツの関心から行われました。さらに高速ですか?
やってみましょう。
作業の途中のどこかで、1つのブロックの読み取りと一連のブロックの読み取りが異なるSDカードコマンドであることに誤って気付きました。それらは、マップドライバーの異なるメソッド-readBlock()およびreadBlocks()で表されます。同様に、1つのブロックに対してコマンドを書き込み、一連のブロックを書き込むことは異なります。MSCドライバーはデフォルトで単位時間あたり1ブロックで動作するように調整されているため、readBlocks()をreadBlock()に置き換えることは理にかなっています。驚いたことに、読み取り速度はさらに増加し、480-500kb / sのレベルになりました!残念ながら、録音機能を使用した同様のトリックでは、速度は向上しませんでした。しかし、最初から1つの質問に悩まされました。読書の写真をもう一度見てみましょう。ノッチ間(1ブロックの読み取り)-約2ms。
SPIクロックが18MHzに設定されています(コアの分周器は72MHzで4です)。理論的には、512バイトの送信は512バイト* 8ビット/ 18 MHz = 228μsを占める必要があります。はい、いくつかのスレッドの同期、キューイングなどに一定のオーバーヘッドが発生しますが、これでは10倍近い違いを説明できません。オシロスコープを使用して、読み取り操作のさまざまな部分にかかる時間を測定しました。運営 | 時間 |
MSCドライバーからカードドライバーへのリクエスト転送(リクエストキューを使用) | <100μs |
読み取りコマンドをマップに送信する | 70mks |
カード待ち | 500-1000μs |
カードから1ブロックを読み取る | 280μs |
応答をMSCドライバーに戻す | <100μs |
驚いたことに、最も長い操作はデータの読み取りではなく、読み取りコマンドと、カードの準備ができてデータを読み取ることができるというカードからの確認の間隔でした。さらに、この間隔は、さまざまなパラメーター(要求の頻度、読み取られるデータのサイズ、読み取られるブロックのアドレス)に応じて大きく変動します。最後のポイントは非常に興味深いです-マップの先頭から遠くに読み取るブロックがあると、読み取りが速くなります(いずれにせよ、これは私の実験マップの場合です)マップに書き込むときに、同様の(しかし悲しい)画像が観察されます。すべてのタイミングを十分に測定することができませんでした。かなり広い範囲で泳ぎましたが、このように見えます。運営 | 時間 |
レコードへのマップコマンドの送信 | 70mks |
カード待ち | 1〜5ミリ秒 |
1ブロックをカードに書き込む | 0.4-1.2ms |
これはすべて、かなり大きなCPU負荷(約75%)によってさらに悪化します。記録自体は、理論的には読み取りと同じ228μsを占有する必要があります。これらは同じ18 MHzでクロックされます。この場合にのみ、FreeRTOSストリームの同期が引き続き表示されます。明らかに、大きなCPU負荷と他の(優先度の高い)スレッドに切り替える必要があるため、合計時間ははるかに長くなります。しかし、最大の悲しみは、カードの準備が整うのを待っていることです。読書の場合よりも何倍も大きい。さらに、ここでカードは100ミリ秒または500ミリ秒も貼り付けることができます。さらに、カードドライバーでは、この部分はアクティブな待機によって実装され、同じ高プロセッサ負荷につながります。ループ内にSysCall :: yield()への呼び出しを追加するコードに分岐がありますが、状況が解決しないのではないかと思います。この呼び出しは、タスクスケジューラが別のスレッドに切り替えることのみを推奨しています。しかし、他のフローはほとんど私と一緒に寝ているので、これは状況を根本的に改善することはありません-マップは愚かなことを止めません。別の面白い瞬間。FreeRTOSでは、コンテキストはSysTick割り込みによって切り替えられます。これはデフォルトで1msに設定されています。このため、オシロスコープの多くの操作は、グリッド上で1ミリ秒単位で調整されます。カードがバカではなく、待機で1ブロックを読み取るのに1ミリ秒未満しかかからない場合、すべてのスレッド、同期、キューを含めて、1ティックで方向を変えることができます。したがって、このモデルの理論上の最大読み取り速度は、正確に500 kb / s(1ミリ秒で0.5 kb)です。何が嬉しい-それは達成されました!
しかし、このことは回避できます。 1msでの整列は、次の理由で発生します。 USBまたはDMAからの割り込みは何にも結び付けられておらず、ティックの途中で発生する可能性があります。割り込みが同期オブジェクトの状態を変更した場合(たとえば、セマフォのロックを解除したり、キューにメッセージを追加した場合)、FreeRTOSはすぐにそれを認識しません。割り込みがジョブを実行すると、制御は割り込み前に動作していたスレッドに転送されます。ティックが終了すると、スケジューラが呼び出され、同期オブジェクトの状態に応じて、対応するストリームに切り替えることができます。
ただし、そのような場合にのみ、FreeRTOSにはスケジューラを強制するメカニズムがあります。先ほど言ったように、割り込みを中断することはできません。ただし、スケジューラーを呼び出す必要があることを示唆することができます(スケジューラーを呼び出すのではなく、呼び出す必要があることを示唆します)。これはまさにportYIELD_FROM_ISR()関数が行うことです。中断後すぐにフローを切り替えるようスケジューラーに依頼します void SdFatSPIDriver::dmaTransferCompletedCB() {
割り込み処理(DMAなどから)が終了すると、スケジューラが呼び出されるハンドラーでPendSV割り込みが自動的に呼び出されます。次に、コンテキストを強制的に切り替え、セマフォを待機していたスレッドに制御を移します。 T.O.
中断に対する応答時間を大幅に短縮できるため、このトリックにより、テストカードでの読み取りを最大600kb / sまで高速化できます。
しかし、これはカードの準備ができるのを長く待たない場合です。残念ながら、カードが長い間考えている場合、読み取りは2ティック(および4〜6書き込み)ストレッチされ、速度は大幅に低下します。さらに、アクティブな待機コードが絶えずカードに侵入し、カードが長時間応答しない場合、ティック全体が通過する可能性があります。この場合、OSスケジューラは、このスレッドの実行時間が長すぎると判断し、通常は他のスレッドに制御を切り替えます。このため、追加の遅延が発生する場合があります。ちなみに、私はこれらすべてを8GBクラス6カードでテストし、手元にある他のカードもいくつか試しました。別のカードも8GBですが、何らかの理由でクラス10は読み取り用に300〜350 kb / sだけを、書き込み用に120 kb / sを提供しました。私が持っていた最大かつ最速のカード-32GBを入れようと思いました。それで最大速度を達成することが可能でした-読み取りのための650kb / sと書き込みのための120kb / s。ところで、私が引用した速度は平均です。瞬間速度を測定するものは何もありませんでした。この分析からどのような結論を導き出すことができますか?- まず、SPIは明らかにSDカードのネイティブインターフェイスではありません。最も一般的な操作では、クールなカードでさえ愚かです。SDIOに目を向けるのは理にかなっています(既にメールでSTM32F103RCT6のバッグを手に入れました-すぐに使えるSDIOサポートがあります)
- -, . . SDIO
- -, ( 4). / . 20 (STM32F103C8T6) 512
おわりに
この記事では、USB MSC実装をSTMicroelectronicsからアップグレードする方法について説明しました。他のSTM32シリーズマイクロコントローラーとは異なり、F103シリーズにはUSB用のDMAサポートが組み込まれていません。しかし、FreeRTOSの助けを借りて、DMA経由でSDカードの読み取り/書き込みを固定することができました。まあ、USBバス帯域幅の使用を最大化するために、ダブルバッファリングを強化することができました。結果は私の期待を超えました。最初は、約400kb / sの速度を目指しましたが、なんとか650kb / sを絞ることができました。しかし、私にとって重要なのは、絶対的な速度インジケータでさえなく、この速度が最小限のプロセッサの介入で達成されるという事実です。したがって、データはDMAおよびUSB周辺機器を使用して送信され、プロセッサは次の操作を充電するためにのみ接続されます。確かに、記録では最高速度を得ることができませんでした-わずか100-120kb / s。 SDカード自体の巨大なタイムアウトの原因。カードはSPI経由で接続されているため、カードの準備状況を確認する方法は他にありません(常にポーリングする方法を除く)。このため、書き込み操作でかなり高いプロセッサ負荷が観察されます。 SDIOを介してカードを接続すると、はるかに高い速度を実現できることを私は秘密に望んでいます。コードを提供するだけでなく、コードがどのように構成されているのか、なぜそのように構築されているのかを説明しようとしました。おそらく、これは他のコントローラーまたはライブラリーに対して同様のことを行うのに役立ちます。私はこれを別のライブラリに割り当てませんでした。このコードは、私のプロジェクトの他の部分とFreeRTOSライブラリに依存しています。さらに、パッチを適用したMSCの実装に基づいてコードを構築しました。したがって、私のバージョンを使用する場合は、元のライブラリにバックポートする必要があります。リポジトリへのリンク:github.com/grafalex82/GPSLoggerSDカードでの作業を高速化する方法に関する建設的なコメントやその他のアイデアを喜んでいます。