:MSC協(xié)議、SD卡驅(qū)動與調(diào)試全解析)
簡介一份基于STM32F103的USB讀卡器完整工程面向嵌入式開發(fā)學(xué)習(xí)者和電子工程師解決如何通過USB與計算機通信并讀寫SD卡的問題。工程涵蓋SPI/SDIO接口設(shè)計、USB Mass Storage類實現(xiàn)、Bulk-Only傳輸協(xié)議處理以及FAT16/FAT32文件系統(tǒng)的移植與操作可幫助讀者搭建一套可實際運行的USB讀卡器。壓縮包共122個文件以54個C源程序和59個頭文件為主輔以2個匯編文件、uvprojx工程文件、可燒錄hex文件和說明文檔整體大小僅422KB便于快速下載與導(dǎo)入開發(fā)環(huán)境。已有1706人學(xué)習(xí)瀏覽適合具備C語言和單片機基礎(chǔ)、希望深入理解USB存儲設(shè)備的開發(fā)者參考。通過該工程可掌握STM32CubeMX外設(shè)配置、HAL庫驅(qū)動編寫、SD卡初始化流程、命令響應(yīng)解析及Bulk傳輸數(shù)據(jù)調(diào)度等實踐技巧結(jié)合完整代碼和清晰目錄結(jié)構(gòu)便于二次開發(fā)與功能擴展。1. 為什么用STM32F103做USB讀卡器一塊幾十塊錢的stm32f103最小系統(tǒng)板一個SD卡槽一根USB線插上電腦就變成一個U盤。這個項目聽起來像網(wǎng)上常見的例程但實際上它有明顯的分水嶺SD卡的SPI讀寫和FatFs文件系統(tǒng)的移植是任何嵌入式教程都會覆蓋的基礎(chǔ)內(nèi)容而要把整個設(shè)備變成一個USB Mass Storage Device需要處理設(shè)備枚舉、端點調(diào)度、SCSI命令響應(yīng)、Bulk-Only傳輸中的CBW/CSW握手。任何一個字節(jié)的時序不對電腦就會報無法識別的USB設(shè)備而且調(diào)試起來無從下手。這篇內(nèi)容從硬件引腳規(guī)劃講起覆蓋USB描述符與SCSI命令分發(fā)、SD卡初始化時序與FatFs的對接最后給出用USB抓包和UART日志定位問題的具體方法。適合正在寫USB設(shè)備固件、想把手頭F103開發(fā)板變成實用工具的人。讀完你不僅能復(fù)現(xiàn)一個讀卡器還能理解為什么電腦會時不時彈出需要格式化磁盤的提示。2. 硬件接口與引腳規(guī)劃SPI/SDIO選型與USB全速布線2.1 48腳和100腳的差距SDIO不是你想用就能用STM32F103的SDIO外設(shè)支持4位總線模式理論讀寫速度可以到幾MB/s是SPI模式的數(shù)倍。但有一個容易被硬件選型忽視的封裝限制SDIO在F103上需要PD2CMD、PA8CLK以及PC8~PC11DATA0~DATA3。如果是常見的STM32F103C8T6最小系統(tǒng)板48引腳封裝只引出了PC13、PC14、PC15PC8~PC11根本沒有物理引腳。也就是說即使芯片內(nèi)部有SDIO外設(shè)也無法在C8T6上接出完整的數(shù)據(jù)線。這種情況下只能退而求其次使用SPI模式。而100引腳及以上的F103ZET6、F103VET6等型號才有完整SDIO信號引出。如果使用SDIO 4位模式還需要注意復(fù)用沖突。SDIO與SPI1、USART3存在引腳復(fù)用重疊例如PD2同時是USART3_RTS。如果電路里還掛了CAN總線PA11/PA12以及PD2的分配就要仔細規(guī)劃。對于大部分自制讀卡器和學(xué)習(xí)項目SPI模式完全夠用。USB全速理論帶寬只有12Mbps約1.5MB/sSPI模式實測讀寫速度在1~2MB/s左右正好卡在USB瓶頸上不會成為明顯短板。我的建議是除非要做大容量高速U盤否則C8T6SPI是最省事的組合也意味著你不需要為了一個讀卡器項目專門換用100腳的大芯片。2.2 SD卡原理圖要點電源去耦、上拉和寫保護SD卡插座電路本身不復(fù)雜但有三個地方容易踩坑這些會在原理圖評審階段被老手盯著看。第一是電源。SD卡在SPI模式下峰值電流可以達到幾十毫安如果直接從STM32板載LDO取電而LDO余量不足卡初始化時會因電壓跌落而無響應(yīng)。常見做法是在卡座VCC引腳并聯(lián)10uF鉭電容和0.1uF陶瓷電容并盡量靠近卡座擺放。第二是上拉。SD卡的SPI模式中MISO、MOSI、SCLK和CS引腳一般可以依靠主機推挽IO驅(qū)動但加上10k上拉能改善長線下的信號邊沿。第三是寫保護檢測??ㄗ腟W引腳會隨卡側(cè)面的撥片輸出高或低電平如果固件忽略這個引腳或者原理圖上讓它懸空就可能出現(xiàn)SD卡沒鎖但是寫保護這種看似沒有邏輯的現(xiàn)象——插卡時彈片接觸不良導(dǎo)致電平波動。SW引腳要經(jīng)10k電阻上拉到3.3V再進MCU做GPIO檢測。下面給一個典型的SPI模式連接表這個表直接決定原理圖怎么畫STM32F103引腳SD卡引腳方向說明PA5 (SPI1_SCK)CLK主機輸出SPI時鐘初始相位極性為模式0PA6 (SPI1_MISO)DO主機輸入卡輸出數(shù)據(jù)配為浮空輸入PA7 (SPI1_MOSI)DI主機輸出主機輸出數(shù)據(jù)PB0CS主機輸出片選低電平有效PA8WP輸入寫保護檢測可選3.3VVCC-加10uF0.1uF去耦GNDGND-共地2.3 USB全速電路與PA11/PA12復(fù)用沖突F103的USB全速設(shè)備端需要一個1.5kΩ上拉電阻接到D線PA12用來告訴主機這是一個全速設(shè)備。很多最小系統(tǒng)板已經(jīng)內(nèi)置了這個電阻。如果自己畫PCB注意這個電阻要接到3.3V不能接5V否則D電平會超過USB規(guī)范范圍。PA11是D-PA12是D它們同時承擔(dān)CAN_RX和CAN_TX的復(fù)用功能。網(wǎng)上常被討論的stm32f103 pa11 bug多半是指把USB和CAN同時配置后PA11上的干擾會導(dǎo)致USB包檢測出錯。我在一個同時使用CAN和USB的項目里就遇到過兩者共同初始化后USB枚舉失敗率明顯上升禁用CAN后恢復(fù)正常。所以如果你的工程既要USB讀卡器又要CAN通信要么分時使用要么把CAN映射到其他引腳實際上F103的CAN固定映射在PA11/PA12無法重映射只能換芯片或放棄CAN。USB信號的PCB走線也要注意。全速信號雖然只有12MHz但邊沿很陡建議D/D-線等長、短而直中間不打過孔不要跨過晶振下方。VBUS感應(yīng)電壓通常通過電阻分壓接到MCU的GPIO或ADC用于檢測USB線是否插入。這里給出標準庫里的USB引腳初始化參考GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // PA11(D-), PA12(D) 配置為復(fù)用推挽輸出交給USB外設(shè)控制 GPIO_InitStructure.GPIO_Pin GPIO_Pin_11 | GPIO_Pin_12; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); // PA8用于VBUS檢測輸入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure);這段代碼做了兩件事先把PA11/PA12配置為復(fù)用推挽輸出讓USB外設(shè)直接驅(qū)動D/D-再把PA8配成帶上拉的輸入用于檢測VBUS電平。這里的重點是GPIO_Mode_AF_PP如果誤配成普通推挽輸出USB外設(shè)無法控制引腳電平枚舉必然失敗。PA8同時是MCO引腳如果板子從PA8輸出了系統(tǒng)時鐘二者會沖突必須改用其他引腳或禁用MCO。3. USB Mass Storage類實現(xiàn)枚舉、描述符與Bulk-Only傳輸3.1 設(shè)備類結(jié)構(gòu)接口描述符定義讀卡器身份USB讀卡器屬于Mass Storage類接口描述符中的bInterfaceClass為0x08bInterfaceSubClass為0x06bInterfaceProtocol為0x50。設(shè)備描述符中的bDeviceClass通常設(shè)置為0讓類信息由接口描述符決定。端點配置上除了默認控制端點EP0外還需要Bulk IN和Bulk OUT兩個端點。F103的USB外設(shè)有8個端點標準庫示例普遍使用EP1 IN和EP1 OUT端點最大包長64字節(jié)。64字節(jié)是F103硬件緩沖區(qū)PMA在內(nèi)存占用和吞吐率之間的一個折中。下面是一份可以直接跑通Windows和Linux MSC驅(qū)動的配置描述符數(shù)組static uint8_t MSC_ConfigDescriptor[] { 0x09, // bLength 0x02, // bDescriptorType Configuration 0x20, 0x00, // wTotalLength 32 0x01, // bNumInterfaces 1 0x01, // bConfigurationValue 1 0x00, // iConfiguration 0x80, // bmAttributes Bus Powered 0x32, // bMaxPower 100mA // Interface Descriptor 0x09, // bLength 0x04, // bDescriptorType Interface 0x00, // bInterfaceNumber 0x00, // bAlternateSetting 0x02, // bNumEndpoints 0x08, // bInterfaceClass Mass Storage 0x06, // bInterfaceSubClass SCSI transparent 0x50, // bInterfaceProtocol Bulk-Only Transport 0x00, // iInterface // Endpoint Descriptor (Bulk IN) 0x07, 0x05, 0x81, 0x02, 0x40, 0x00, 0x00, // Endpoint Descriptor (Bulk OUT) 0x07, 0x05, 0x01, 0x02, 0x40, 0x00, 0x00 };配置描述符總共32字節(jié)其中端點描述符部分0x81是EP1 IN0x01是EP1 OUT0x02表示Bulk傳輸0x40是端點最大包長64字節(jié)。注意F103內(nèi)部把IN和OUT的同一個端點號視為一個端點主機看到的是地址0x81和0x01兩個方向?qū)嶋H占用的PMA緩沖區(qū)可按兩個64字節(jié)塊分配。很多移植失敗是因為PMA緩沖區(qū)首地址設(shè)置重疊導(dǎo)致收發(fā)的數(shù)據(jù)互相覆蓋。3.2 CBW/CSW狀態(tài)機的實現(xiàn)方式Bulk-Only傳輸協(xié)議規(guī)定主機先通過Bulk OUT端點發(fā)送31字節(jié)的CBWCommand Block Wrapper包括簽名、標記、傳輸長度、方向、LUN、命令塊長度和16字節(jié)的SCSI命令。設(shè)備執(zhí)行命令后通過Bulk IN端點返回13字節(jié)的CSWCommand Status Wrapper。CSW中的bCSWStatus為0表示成功1表示失敗2則表示命令階段錯誤。許多初學(xué)者寫出來的設(shè)備能在枚舉階段被識別但插上后Windows一直轉(zhuǎn)圈就是因為設(shè)備沒有在正確的時機發(fā)送CSW。下面是一個精簡的SCSI命令分發(fā)骨架uint8_t MSC_ProcessCmd(uint8_t *cbw_cb, uint8_t lun) { uint32_t lba, cnt; switch (cbw_cb[0]) { case 0x00: // TEST UNIT READY return 0; case 0x25: // READ CAPACITY buf[0] (g_sd_capacity - 1) 24; buf[1] (g_sd_capacity - 1) 16; buf[2] (g_sd_capacity - 1) 8; buf[3] (g_sd_capacity - 1) 0xFF; buf[4] 0x00; buf[5] 0x00; buf[6] 0x02; buf[7] 0x00; // 塊大小512 return 0; case 0x12: // INQUIRY memcpy(buf, STM32 MASS STORAGE , 24); buf[0] 0x00; // 直接訪問設(shè)備 buf[1] 0x80; // 可移除介質(zhì) buf[2] 0x02; // SCSI-2 buf[3] 0x02; // 響應(yīng)數(shù)據(jù)格式版本 return 0; case 0x28: // READ(10) lba (cbw_cb[2] 24) | (cbw_cb[3] 16) | (cbw_cb[4] 8) | cbw_cb[5]; cnt (cbw_cb[7] 8) | cbw_cb[8]; return SD_ReadBlocks(lun, lba, buf, cnt); case 0x2A: // WRITE(10) lba (cbw_cb[2] 24) | (cbw_cb[3] 16) | (cbw_cb[4] 8) | cbw_cb[5]; cnt (cbw_cb[7] 8) | cbw_cb[8]; return SD_WriteBlocks(lun, lba, buf, cnt); case 0x03: // REQUEST SENSE buf[0] 0x70; buf[2] 0x00; buf[7] 0x0A; return 0; default: return -1; // 不支持的命令由上層回應(yīng)STALL } }這里READ(10)/WRITE(10)解析了5字節(jié)LBA和2字節(jié)扇區(qū)數(shù)然后直接調(diào)用SD卡底層讀寫函數(shù)。REQUESTSENSE被簡化成無錯誤應(yīng)答這在大多數(shù)Windows主機上夠用。如果主機發(fā)出0x1EPREVENT ALLOW MEDIUM REMOVAL之類的命令也應(yīng)該返回成功否則可移動磁盤彈不出。注意READ CAPACITY返回的是邏輯塊地址最后一個扇區(qū)的編號不是扇區(qū)總數(shù)所以用(g_sd_capacity - 1)。g_sd_capacity來自SD卡CSD寄存器解析單位是扇區(qū)。3.3 枚舉成功不等于能被識別為磁盤USB枚舉成功只說明設(shè)備進入了地址狀態(tài)主機還會發(fā)送一系列SCSI命令來確認它是磁盤。如果INQUIRY返回的數(shù)據(jù)不符合規(guī)范或者READ CAPACITY返回的容量小于某個閾值Windows可能把它識別為未知設(shè)備或0字節(jié)U盤。另一種常見問題是設(shè)備沒有正確響應(yīng)CBW導(dǎo)致主機用控制端點發(fā)送CLASS請求失敗。這時你會看到設(shè)備管理器里的錯誤碼43。要定位這類問題最快的辦法是用USB抓包分析實際收發(fā)內(nèi)容這一節(jié)的具體操作留到第5章。除了正確響應(yīng)命令設(shè)備還必須處理CBW中的dCBWTransferLength。當(dāng)這個長度與SCSI命令實際數(shù)據(jù)長度不一致時設(shè)備不能簡單忽略必須讀完或丟棄剩余數(shù)據(jù)再發(fā)送CSW。我的做法是在USB接收中斷里維護一個全局剩余字節(jié)計數(shù)直到計數(shù)為0才允許發(fā)送CSW。如果你想更快驗證協(xié)議正確性可以先用ST官方USB庫中現(xiàn)成的usb_scsi.c它已經(jīng)實現(xiàn)了完整的狀態(tài)機之后按需求裁剪或改寫。4. SD卡協(xié)議與FatFs文件系統(tǒng)初始化時序、SPI模式讀寫和FAT32掛載4.1 SD卡初始化流程與時序細節(jié)SD卡在SPI模式下初始化有嚴格時序上電后先發(fā)送至少74個時鐘周期的高電平讓卡完成內(nèi)部上電過程并進入SPI模式。然后發(fā)送CMD00x40卡返回R1響應(yīng)0x01表示進入空閑狀態(tài)。接著發(fā)送CMD8檢測卡是否支持SD v2.0再根據(jù)結(jié)果發(fā)送ACMD41。ACMD41必須先發(fā)CMD550x77作為前綴。初始化階段SPI時鐘必須低于400kHz完成后才能提高到高速模式。這里給出一個關(guān)鍵初始化序列static uint8_t SD_Init_SPI(void) { uint8_t resp; uint16_t i; // 發(fā)送至少80個時鐘脈沖CS保持高 CS_HIGH(); for (i 0; i 10; i) { SPI_SendData(0xFF); // 每個字節(jié)產(chǎn)生8個時鐘 } CS_LOW(); resp SD_SendCmd0(); // CMD0, 期望R10x01 if (resp ! 0x01) return 0xFF; resp SD_SendCmd8(); // CMD8, 檢測SD v2.0 if (resp 0x01) { // 支持SD v2.0置HCS位重試ACMD41 do { resp SD_SendAcmd41(0x40000000); } while (resp ! 0x00); } else if (resp 0x05) { // 僅支持SD v1.x不置HCS do { resp SD_SendAcmd41(0x00000000); } while (resp ! 0x00); } else { return 0xFF; } resp SD_SendCmd58(); // 讀OCR if (resp 0x00) { uint32_t ocr SD_ReadOCR(); if (ocr 0x40000000) { g_sd_type SD_TYPE_SDHC; } else { g_sd_type SD_TYPE_SD; } } SD_SendCmd16(512); // 設(shè)置塊長度 SPI_SetSpeed(SPI_SPEED_HIGH); // 提高時鐘 return 0x00; }這段代碼有兩個容易出錯的地方。一是CMD8返回0x05表示0x40命令非法說明卡可能是MMC或老版本SD這時ACMD41不能置HCS位如果強制置位卡永遠不會返回0x00。二是CMD58之后緊接著讀取OCR的4字節(jié)數(shù)據(jù)必須在同一個片選周期內(nèi)讀完否則卡會結(jié)束響應(yīng)。有些代碼直接調(diào)用發(fā)送命令的函數(shù)但忽略后續(xù)數(shù)據(jù)讀取導(dǎo)致OCR始終為空進而把SDHC卡識別成普通SD卡。4.2 FatFs在這里的定位裸機自檢而不是讀卡器固件的必要部分一個常見的認知誤區(qū)是USB讀卡器固件里必須同時有SD卡驅(qū)動和FAT文件系統(tǒng)。實際上USB MSC對主機暴露的是一個塊設(shè)備主機上的操作系統(tǒng)會自己解析SD卡上的MBR和FAT32表設(shè)備端不需要也不應(yīng)該感知文件系統(tǒng)。但我在調(diào)試項目時一定會先在STM32裸機上移植FatFs跑一輪文件讀寫自檢。原因很簡單把SPI驅(qū)動、SD卡協(xié)議和FatFs三層綁定在一起測試可以快速隔離問題。如果FatFs能在設(shè)備上創(chuàng)建和讀取文件說明SD卡底層沒問題隨后調(diào)試USB MSC時出現(xiàn)故障問題就鎖定在USB協(xié)議層而不是SD卡驅(qū)動層。FatFs源碼只有ff.c、ff.h、diskio.c、diskio.h四個核心文件。移植需要實現(xiàn)disk_initialize、disk_read、disk_write、disk_status、disk_ioctl和get_fattime。下面是一個disk_read的常見實現(xiàn)DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { UINT i; if (pdrv) return RES_PARERR; // 單塊讀取方式如果count1建議使用CMD18多塊讀 for (i 0; i count; i) { if (SD_ReadSingleBlock(sector i, buff (i * 512)) ! 0) { return RES_ERROR; } } return RES_OK; }參數(shù)pdrv是物理驅(qū)動器編號單卡時固定為0。sector是邏輯塊地址FatFs傳入的偏移不包含任何分區(qū)信息因為讀卡器場景下主機把整張卡當(dāng)作塊設(shè)備。count是要讀的扇區(qū)數(shù)如果直接使用循環(huán)單塊讀性能會很差。我在自己的驅(qū)動里實現(xiàn)了CMD18多塊讀取一次讀取可以連續(xù)傳輸任意塊數(shù)。這樣USB READ(10)按64KB批量傳輸時底層不會每秒產(chǎn)生上百次命令往返。4.3 與USB MSC對接的緩沖區(qū)和扇區(qū)映射F103的PMA緩沖區(qū)是512字節(jié)上限實際對于USB全速是多個端點共享512字節(jié)而SD卡一個扇區(qū)就是512字節(jié)這個巧合讓緩沖區(qū)設(shè)計變得簡單。常見的做法是在全局開辟一個512字節(jié)的塊緩沖區(qū)USB外設(shè)把數(shù)據(jù)放到這個緩沖區(qū)然后SD卡驅(qū)動從同一塊內(nèi)存寫出。但要注意如果USB中斷正在寫入緩沖區(qū)的后半部分而SD卡DMA開始讀前半部分數(shù)據(jù)就會錯位。這個問題在寫入場景尤其明顯因為USB接收是流式的SD卡寫入是塊式的。我的方案是給每個端點分配兩個64字節(jié)USB緩沖區(qū)雙緩沖當(dāng)?shù)谝粋€64字節(jié)填滿并轉(zhuǎn)入SD卡預(yù)寫區(qū)時USB外設(shè)立刻切到第二塊64字節(jié)繼續(xù)接收。這樣可以連續(xù)接收而不丟數(shù)據(jù)同時避免用大塊內(nèi)存做雙份拷貝。如果只是學(xué)習(xí)驗證可以先只使用阻塞式傳輸每次收到完整512字節(jié)再放行USB端點。主機端USB驅(qū)動允許設(shè)備在寫操作時短暫NAK但NAK次數(shù)過多會導(dǎo)致寫入速度降到100KB/s以下所以雙緩沖是必做項。關(guān)于寫保護在disk_status和SCSI MODE SENSE命令里要正確返回。當(dāng)SD卡SW引腳檢測到寫保護時WRITE(10)命令應(yīng)返回錯誤或直接STALL。否則主機在寫操作成功后會誤認為數(shù)據(jù)已寫入但卡實際拒絕寫入最終導(dǎo)致文件系統(tǒng)損壞。5. 調(diào)試與排錯USB抓包、UART日志和常見坑5.1 先隔離問題域UART打印初始化狀態(tài)調(diào)試USB讀卡器最怕電腦上完全找不到設(shè)備因為你無法只靠USB現(xiàn)象判斷是SD卡初始化失敗還是USB枚舉失敗。我的做法是復(fù)用USART1用一個FT231X usb uart模塊把調(diào)試日志打印到電腦固件里每個關(guān)鍵節(jié)點都輸出一行。例如CMD0_RESP、CMD8_RESP、ACMD41_RETRY、OCR_VALUE、USB_SUSPEND等。插上USB后如果日志里CMD8在0x01和0x00之間反復(fù)跳動說明卡座接觸不良如果初始化流程根本沒開始則說明USB供電或復(fù)位有問題。這比接個邏輯分析儀去戳SD卡引腳更高效。5.2 用USBPcap和Wireshark看SCSI流量usb抓包在Windows下的標準做法是安裝USBPcap驅(qū)動然后在Wireshark里選擇\\.\USBPcap1接口。抓包后會看到大段的URB_BULK_IN和URB_BULK_OUT。過濾條件可以直接寫usb.transfer_type 0x03再配合usb.bmRequestType篩選MSC類請求。重點是看CBW的前四個字節(jié)是否為0x43425355這是USBC的字節(jié)序以及CSW的簽名0x53425355。如果設(shè)備返回了CSW但狀態(tài)字節(jié)為2說明設(shè)備驅(qū)動遇到了不支持的命令或者命令執(zhí)行失敗。Wireshark能直接解析MSC協(xié)議把每個SCSI命令展開成可讀字段這個能力比任何printf都有用。5.3 常見坑速查表現(xiàn)象可能原因解決辦法電腦提示無法識別的USB設(shè)備D上拉電阻缺失或USB時鐘不對檢查PA12到3.3V的1.5k電阻確認晶振為8MHzSWD下載正常但USB無響應(yīng)USB時鐘未使能或PA11/PA12被復(fù)用在RCC中使能USB時鐘關(guān)閉CAN復(fù)用功能SD卡初始化卡死SPI時鐘過快或CS未拉低初始化階段SPI分頻128CS低電平保持到命令結(jié)束識別后顯示0字節(jié)磁盤READ CAPACITY扇區(qū)數(shù)錯誤檢查CSD寄存器解析注意最后一個LBA要減1寫入幾分鐘后文件損壞USB緩沖區(qū)與SD緩沖區(qū)重疊使用雙緩沖避免同一內(nèi)存被USB和SD同時訪問卡沒鎖卻提示寫保護卡座SW引腳懸空或抖動SW加上拉電阻并在固件里做10ms軟件消抖5.4 用FatFs當(dāng)體檢報告縮小問題范圍這里分享一個能明顯提高調(diào)試效率的技巧在寫USB MSC代碼之前先讓FatFs在SPI上跑一遍完整流程。具體做法是在main里初始化SD卡后調(diào)用f_mount掛載然后用f_open創(chuàng)建test.txt并寫入一段字符串再讀出來比較。如果全部通過就說明SPI時序、SD卡協(xié)議、DMA緩存都沒問題。之后遇到電腦上無法打開磁盤時可以先在固件里強制創(chuàng)建文件如果文件在PC上可見說明MSC層是通的問題可能出在主機側(cè)驅(qū)動如果文件不可見則單獨查MSC命令。這樣把調(diào)試范圍縮小一半遠比對著抓包數(shù)據(jù)瞎猜更快。本文還有配套的精品資源點擊獲取