FreeRTOSずDMAを䜿甚しお、USB倧容量ストレヌゞデバむスをSTM32F103にポンプしたす。

画像

最近、 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 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) { // Start data transfer memset(buf, 0xff, n); HAL_SPI_TransmitReceive_DMA(&spiHandle, buf, buf, n); // Wait until transfer is completed xSemaphoreTake(xSema, 100); return 0; // Ok status } void SdFatSPIDriver::send(const uint8_t* buf, size_t n) { // Start data transfer HAL_SPI_Transmit_DMA(&spiHandle, (uint8_t*)buf, n); // Wait until transfer is completed xSemaphoreTake(xSema, 100); } void SdFatSPIDriver::dmaTransferCompletedCB() { // Resume SD thread xSemaphoreGiveFromISR(xSema, NULL); }
      
      







ここでの党䜓の魅力は、倧きなデヌタブロックを読み曞きする必芁があるずき、このコヌドが完了を埅たないこずです。 代わりに、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; // Not used if(!card.readBlocks(blk_addr, buf, blk_len)) return USBD_FAIL; return (USBD_OK); } int8_t SD_MSC_Write (uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) { (void)lun; // Not used if(!card.writeBlocks(blk_addr, buf, blk_len)) return USBD_FAIL; return (USBD_OK); }
      
      







既に述べたように、割り蟌みの䞋から呌び出された堎合、カヌドドラむバヌは機胜したせん。 しかし、通垞のスレッドではうたく機胜したす。 それでは、別のスレッドを開始したしょう。



このスレッドは、キュヌを介しお読み取りおよび曞き蟌み芁求を受け取りたす。 各芁求には、操䜜の皮類読み取り/曞き蟌み、読み取りたたは曞き蟌みを行うブロックの数、ブロックの数、およびデヌタバッファヌぞのポむンタヌに関する情報が含たれたす。 たた、操䜜のコンテキストぞのポむンタも取埗したした。少し埌で必芁になりたす。



読み取り/曞き蟌み芁求キュヌ
 enum IOOperation { IO_Read, IO_Write }; struct IOMsg { IOOperation op; uint32_t lba; uint8_t * buf; uint16_t len; void * context; }; // A queue of IO commands to execute in a separate thread QueueHandle_t sdCmdQueue = NULL; // Initialize thread responsible for communication with SD card bool initSDIOThread() { // Initialize synchronisation sdCmdQueue = xQueueCreate(1, sizeof(IOMsg)); bool res = card.begin(&spiDriver, PA4, SPI_FULL_SPEED); return res; }
      
      







スレッド自䜓はコマンドを埅っおスリヌプしおいたす。 コマンドが到着するず、目的の操䜜がさらに同期的に実行されたす。 操䜜の最埌にコヌルバックを呌び出したす。コヌルバックは、実装に応じお、読み取り/曞き蟌み操䜜の最埌に必芁な凊理を行いたす。



カヌドぞの読み取り/曞き蟌みを提䟛するストリヌム
 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) { // Send read command to IO executor thread IOMsg msg; msg.op = IO_Read; msg.lba = blk_addr; msg.len = blk_len; msg.buf = buf; msg.context = context; if(xQueueSendFromISR(sdCmdQueue, &msg, NULL) != pdPASS) return USBD_FAIL; return (USBD_OK); } int8_t SD_MSC_Write (uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len, void * context) { // Send read command to IO executor thread IOMsg msg; msg.op = IO_Write; msg.lba = blk_addr; msg.len = blk_len; msg.buf = buf; msg.context = context; if(xQueueSendFromISR(sdCmdQueue, &msg, NULL) != pdPASS) return USBD_FAIL; return (USBD_OK); }
      
      







重芁なポむントがありたす-これらの関数のセマンティクスが倉曎されたした。 珟圚、それらは非同期、぀たり 操䜜の実際の終了を埅たないでください。 そのため、それらを呌び出すコヌドを埮調敎する必芁がありたすが、これに぀いおは少し埌で説明したす。



それたでの間、これらの関数をテストするために、別のテストスレッドを䜜成したす。 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カヌドストリヌムに送信したす。 そこで、同期的に読み取りが行われ、コヌルバックを介しお終了が報告されたす。 このコヌルバックの実装を眮き換えたした。このコヌルバックは、テストスレッドに続行できるこずを通知するだけですテストスレッドは、ulTask​​NotifyTake関数で垞にスリヌプ状態になっおいたす。



しかし、最も重芁なこずは、このバヌゞョンの読み取り速床は玄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ドラむバヌアルゎリズムは次のようになりたす





このスキヌムで最も長い操䜜は、実際にはカヌドからの読み取りです。 私の枬定によるず、同期モヌドでの読み取りには玄2〜3msかかりたす。 さらに、転送はプロセッサヌによっお行われ、これはすべお割り蟌みUSBで行われたす。 比范のために、DMAを介しお長さ512の1ブロックを枛算するには、1ミリ秒以䞊かかりたす。



デヌタの読み取りを倧幅にたずえば1Mb / sたでスピヌドアップできたせんでした-これは、SPIを介しお接続されたカヌドの垯域幅です。 しかし、トランザクションの間に1ミリ秒の䌑止をサヌビスに眮くこずを詊みるこずができたす。



このように芋えたすわずかに簡略化されおいたす





SCSI_ProcessRead関数は「前」ず「埌」に簡単に分割されるため、このアプロヌチを実装しおみたしょう。 ぀たり、読み取りを開始するコヌドは割り蟌みのコンテキストで実行され、残りのコヌドはコヌルバックに移動したす。 このコヌルバックのタスクは、読み取られたデヌタを出力バッファヌにプッシュするこずですホストは䜕らかの方法で察応する芁求でこのデヌタを取埗したす



非同期読み取りに適合したSCSI_ProcessRead関数
 /** * @brief SCSI_ProcessRead * Handle Read Process * @param lun: Logical unit number * @retval status */ 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; /* case 6 : Hi = Di */ 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぀の理由で、それらだけが私たちに合わないでしょう。



  1. マむクロコントロヌラ自䜓が提䟛するダブルバッファリングを䜿甚するには、USBコアずMSC実装を再描画する必芁がある堎合がありたす
  2. バッファサむズは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; } // Synchronization to avoid several transmits at a time // This must be located here as it waits finishing previous USB transfer // while the code below prepares next one pdev->pClassSpecificInterfaceMSC->OnFinishOp(); // Save these values for transmitting data uint8_t * txBuf = hmsc->bot_data + hmsc->bot_data_idx * MSC_MEDIA_PACKET; uint16_t txSize = len; // But before transmitting set the correct state // Note: we are in context of SD thread, not the USB interrupt // So values have to be correct when DataIn interrupt occurrs hmsc->scsi_blk_addr += len; hmsc->scsi_blk_len -= len; /* case 6 : Hi = Di */ hmsc->csw.dDataResidue -= len; if (hmsc->scsi_blk_len == 0) { hmsc->bot_state = USBD_BOT_LAST_DATA_IN; } else { hmsc->bot_data_idx ^= 1; hmsc->bot_data_length = MSC_MEDIA_PACKET; SCSI_ProcessRead(pdev, lun); // Not checking error code - SCSI_ProcessRead() already enters error state in case of read failure } // Now we can transmit data read from SD USBD_LL_Transmit (pdev, MSC_IN_EP, txBuf, txSize); }
      
      







この関数は、構造をわずかに倉曎したした。たず、ここでダブルバッファリングが実装されたす。この関数はカヌドからの読み取りが終了するず呌び出されるため、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、デヌタ入力むベントによっお呌び出されたす
 /** * @brief SCSI_Read10 * Process Read10 command * @param lun: Logical unit number * @param params: Command parameters * @retval status */ static int8_t SCSI_Read10(USBD_HandleTypeDef *pdev, uint8_t lun , uint8_t *params) { USBD_MSC_BOT_HandleTypeDef *hmsc = pdev->pClassDataMSC; // Synchronization to avoid several transmits at a time pdev->pClassSpecificInterfaceMSC->OnStartOp(); if(hmsc->bot_state == USBD_BOT_IDLE) /* Idle */ { // Params checking 
 hmsc->scsi_blk_addr = ... hmsc->scsi_blk_len = ... hmsc->bot_state = USBD_BOT_DATA_IN; ... hmsc->bot_data_idx = 0; hmsc->bot_data_length = MSC_MEDIA_PACKET; return SCSI_ProcessRead(pdev, lun); } return 0; }
      
      







SCSI_Read10の元の実装では、関数ぞの最初の呌び出しでパラメヌタヌがチェックされ、最初のブロックの読み取りプロセスが開始されたした。前のパケットが既に送信されおいお、次のパケットの読み取りを開始する必芁があるずきにDataInが䞭断されるず、埌で同じ関数が呌び出されたす。䞡方のブランチは、SCSI_ProcessRead関数を䜿甚しお読み取りを開始したした。



新しい実装では、SCSI_ProcessReadの呌び出しはif内に移動し、最初のブロックbot_state == USBD_BOT_IDLEを読み取るためだけに呌び出され、埌続のブロックの読み取りはcardReadCompletedCBから開始されたす。



その結果を芋おみたしょう。オシロスコヌプでそのようなノッチを芋るために、ブロックの読み取りの間に意図的にわずかな遅延を远加したした。実際、読み取り操䜜の間隔が非垞に短いため、オシロスコヌプにはこれが衚瀺されたせん。



画像



この写真からわかるように、アむデアは成功でした。前の操䜜が終了するずすぐに、新しい読み取り操䜜が開始されたす。読み取り間の䞀時停止は非垞に小さく、䞻にホストによっお決定されたすトランザクション間の1ミリ秒の同じ遅延。倧きなファむルの平均読み取り速床は400〜440kb / sに達し、非垞に良奜です。最埌に、プロセッサの負荷は玄2です。



しかし、蚘録はどうですか



カヌドに蚘録するずいう話題を巧みに避けながら。しかし、珟圚では、MSCドラむバヌの知識ず理解があれば、蚘録機胜の実装を耇雑にする必芁はありたせん。



元の実装は次のように動䜜したす。





最初に、元の実装をWrite関数の非同期に適合させたす。SCSI_ProcessWrite関数を分離し、コヌルバックで残りの半分を呌び出すだけです。



レコヌド機胜の実装
 /** * @brief SCSI_ProcessWrite * Handle Write Process * @param lun: Logical unit number * @retval status */ 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); // Check error code first if(res != 0) { SCSI_SenseCode(pdev, lun, HARDWARE_ERROR, WRITE_FAULT); return; } hmsc->scsi_blk_addr += len; hmsc->scsi_blk_len -= len; /* case 12 : Ho = Do */ hmsc->csw.dDataResidue -= len; if (hmsc->scsi_blk_len == 0) { MSC_BOT_SendCSW (pdev, USBD_CSW_CMD_PASSED); } else { /* Prepare EP to Receive next packet */ USBD_LL_PrepareReceive (pdev, MSC_OUT_EP, hmsc->bot_data, 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 /* Data Out state for the first receiving block */
      
      







そしおその凊理
 /** * @brief MSC_BOT_DataOut * Process MSC OUT data * @param pdev: device instance * @param epnum: endpoint index * @retval None */ 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カヌドストリヌムの蚘録キュヌのサむズを倉曎するこずを忘れないでください



キュヌの初期化
 // Initialize thread responsible for communication with SD card bool initSDIOThread() { // Initialize synchronisation sdCmdQueue = xQueueCreate(2, sizeof(IOMsg)); 
 }
      
      







SCSI_Write10関数はほずんど倉曎されおいたせん。ダブルバッファヌむンデックスの初期化ずUSBD_BOT_DATA_OUT_1ST状態ぞの移行のみが远加されおいたす。



SCSI_Write10関数
 /** * @brief SCSI_Write10 * Process Write10 command * @param lun: Logical unit number * @param params: Command parameters * @retval status */ 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) /* Idle */ { // Checking params 
 hmsc->scsi_blk_addr = ... hmsc->scsi_blk_len = ... /* Prepare EP to receive first data packet */ hmsc->bot_state = USBD_BOT_DATA_OUT_1ST; hmsc->bot_data_idx = 0; USBD_LL_PrepareReceive (pdev, MSC_OUT_EP, hmsc->bot_data, MIN (hmsc->scsi_blk_len, MSC_MEDIA_PACKET)); } else /* Write Process ongoing */ { return SCSI_ProcessWrite(pdev, lun); } return 0; }
      
      







最も興味深いロゞックはすべおSCSI_ProcessWrite関数に集䞭したす-これはバッファヌが割り圓おられ、読み取りずレコヌドのチェヌン党䜓が構築される堎所です。



SCSI_ProcessWrite関数
 /** * @brief SCSI_ProcessWrite * Handle Write Process * @param lun: Logical unit number * @retval status */ 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; // Figure out what to do after writing the block if(hmsc->scsi_blk_len == len) { ctxt->next_write_len = 0xffffffff; } else if(hmsc->scsi_blk_len == len + MSC_MEDIA_PACKET) { ctxt->next_write_len = 0; } else { ctxt->next_write_len = MIN(hmsc->scsi_blk_len - 2 * MSC_MEDIA_PACKET, MSC_MEDIA_PACKET); } // Prepare other fields of the context ctxt->buf = hmsc->bot_data + hmsc->bot_data_idx * MSC_MEDIA_PACKET; ctxt->pdev = pdev; // Do not allow several receives at a time if(hmsc->bot_state != USBD_BOT_DATA_OUT_1ST) pdev->pClassSpecificInterfaceMSC->OnStartOp(); // Write received data if(pdev->pClassSpecificInterfaceMSC->Write(lun , ctxt->buf, hmsc->scsi_blk_addr / hmsc->scsi_blk_size, len / hmsc->scsi_blk_size, ctxt) < 0) { SCSI_SenseCode(pdev, lun, HARDWARE_ERROR, WRITE_FAULT); return -1; } // Switching blocks hmsc->bot_data_idx ^= 1; hmsc->scsi_blk_addr += len; hmsc->scsi_blk_len -= len; /* case 12 : Ho = Do */ hmsc->csw.dDataResidue -= len; // Performing one extra receive for the first time in order to run receive and write operations in parallel if(hmsc->bot_state == USBD_BOT_DATA_OUT_1ST && hmsc->scsi_blk_len != 0) { hmsc->bot_state = USBD_BOT_DATA_OUT; USBD_LL_PrepareReceive (pdev, MSC_OUT_EP, hmsc->bot_data + hmsc->bot_data_idx * MSC_MEDIA_PACKET, // Second buffer MIN (hmsc->scsi_blk_len, MSC_MEDIA_PACKET)); } return 0; }
      
      







たず、ここで蚘録コンテキストが準備されおいたす-コヌルバックに送信される情報。特に、このブロックの蚘録が終了したずきの凊理を決定したす。





デヌタブロックがカヌドの曞き蟌みキュヌに送信された埌、バッファヌむンデックス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; // Check error code first if(res != 0) { SCSI_SenseCode(pdev, lun, HARDWARE_ERROR, WRITE_FAULT); return; } if (ctxt->next_write_len == 0xffffffff) { MSC_BOT_SendCSW (pdev, USBD_CSW_CMD_PASSED); } else { pdev->pClassSpecificInterfaceMSC->OnFinishOp(); if(ctxt->next_write_len != 0) { /* Prepare EP to Receive next packet */ USBD_LL_PrepareReceive (pdev, MSC_OUT_EP, ctxt->buf, ctxt->next_write_len); } } }
      
      







最埌の和音は同期であり、その本質は写真に衚​​瀺しやすいです。



画像



非垞にたれですが、それでも、次のパケットを受信する前にカヌドぞの曞き蟌みが終了する堎合がありたす。その結果、コヌドは同期がなかった堎合別のパケットを芁求できたすが、珟圚のパケットはただ完党に受信されおいたせん。これを防ぐには、同期を远加する必芁がありたした。これで、次のブロックの受信を芁求する前に、コヌドは前のブロックの受信が終了するたで埅機したす。読み取り時に䜿甚された同期ツヌル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ミリ秒も貌り付けるこずができたす。さらに、カヌドドラむバヌでは、この郚分はアクティブな埅機によっお実装され、同じ高プロセッサ負荷に぀ながりたす。



カヌドの準備を積極的に埅っおいたす
 // wait for card to go not busy bool SdSpiCard::waitNotBusy(uint16_t timeoutMS) { uint16_t t0 = curTimeMS(); while (spiReceive() != 0XFF) { if (isTimedOut(t0, timeoutMS)) { return false; } } return true; }
      
      







ルヌプ内に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() { // Resume SD thread BaseType_t xHigherPriorityTaskWoken; xSemaphoreGiveFromISR(xSema, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }
      
      







割り蟌み凊理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。ずころで、私が匕甚した速床は平均です。瞬間速床を枬定するものは䜕もありたせんでした。



この分析からどのような結論を導き出すこずができたすか





おわりに



この蚘事では、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/GPSLogger



SDカヌドでの䜜業を高速化する方法に関する建蚭的なコメントやその他のアむデアを喜んでいたす。



All Articles