移植實踐)
簡介面向嵌入式開發(fā)者的FATFS文件系統(tǒng)R0.14b完整源代碼包適合STM32及其他ARM平臺項目中需要為存儲介質(zhì)提供FAT讀寫能力的場景。該版本在可移植性與內(nèi)存占用上做了優(yōu)化通過diskio驅動適配層屏蔽硬件差異開發(fā)者只需編寫底層扇區(qū)讀寫函數(shù)即可完成集成并可根據(jù)需要裁剪FAT12/16/32、長文件名等功能選項。包體共89個文件壓縮后僅1.84MB其中以10個C源文件與3個頭文件組成的源碼主體為核心附帶53個HTML幫助文檔、16個PNG示意圖、配置與更新說明等便于對照學習與快速查閱。源碼包內(nèi)含完整目錄結構區(qū)分文檔、資源與源代碼模塊并保留LICENSE等授權信息有助于理解驅動層、文件系統(tǒng)層和配置接口之間的關系縮短移植與調(diào)試周期。當前已有603人學習下載適合希望深入掌握嵌入式文件系統(tǒng)原理或正在實施存儲相關功能的開發(fā)者。 我在一個量產(chǎn)的項目里連續(xù)寫了幾年 STM32 的存儲模塊凡是涉及 SD 卡、SPI Flash 數(shù)據(jù)記錄的設備基本都繞不開 FATFS 這套文件系統(tǒng)。上一批設備的固件里用的就是 FATFS R0.14b發(fā)布于 2021 年 4 月 17 日。這個版本到今天也不算新但依舊是很多嵌入式工程師的默認選擇——原因很簡單它足夠穩(wěn)定代碼結構干凈資源占用可控踩過的坑大多有現(xiàn)成答案。如果你手里正有一顆 STM32 或者類似的 MCU想接一張 SD 卡或者 SPI Flash 做點數(shù)據(jù)記錄遲早要跟這套源碼打交道。這篇我就從源碼角度把 R0.14b 掰開揉碎講一遍包括版本定位、各層代碼怎么協(xié)作、移植到實際硬件的完整流程以及我在這過程中總結出來的幾個典型問題和排查思路。1. 為什么 R0.14b 這顆老版本依然值得細讀先聊版本。FATFS 是 ChaN 開發(fā)的開源 FAT 文件系統(tǒng)模塊面向小型嵌入式系統(tǒng)主要支持 FAT32、FAT16、FAT12從某個版本開始支持 exFAT。R0.14b 是 2021 年 4 月的維護版本這個版本本身的定位很明確在 R0.14a 的基礎上修復邊界問題尤其是 exFAT 相關的文件命名、時間處理和長分區(qū)處理。也就是說它不是那種大改架構的版本而是把上一版引入的功能打磨穩(wěn)定的版本。對做產(chǎn)品的工程師來說這種剛穩(wěn)定下來的版本往往比最新版本更有吸引力。最新版本可能功能更多但也會帶來新的配置項和行為變化R0.14b 則處于一個相當不錯的平衡點exFAT 和 64 位 LBA 支持已經(jīng)可用同時整個模塊的代碼量和內(nèi)存占用仍然維持在很低的水平。官方文檔里給的資源占用參考是 ROM 大約 30KB 左右RAM 取決于緩沖區(qū)和長文件名配置最小可以壓到幾百字節(jié)。對于大多數(shù) MCU 工程來說這個開銷完全在可接受范圍內(nèi)。還有一個原因值得強調(diào)R0.14b 的配置宏體系已經(jīng)非常成熟。你打開ffconf.h就會看到從FF_USE_MKFS到FF_FS_LOCK一整套完整的裁剪選項幾乎每個模塊都能單獨開關。這意味著你可以只保留自己需要的功能把剩下的代碼全部排除在編譯之外既省 Flash 又降低出問題的概率。1.1 從 R0.14a 到 R0.14b變化集中在哪如果只從使用角度感受R0.14a 和 R0.14b 之間的差異不算大。但如果你去翻源碼目錄下的history.txt會發(fā)現(xiàn) R0.14b 的修復點都在細節(jié)上exFAT 卷上刪除文件后的目錄項清理、長文件名的邊界匹配邏輯、以及某些系統(tǒng)下f_mkfs創(chuàng)建分區(qū)表時的對齊問題。這些場景在日常調(diào)試里不一定能碰到但一旦碰到就是詭異問題比如文件刪掉了磁盤空間卻不釋放或者長文件名的文件在某些播放器里顯示亂碼。R0.14b 就是在替這些邊緣情況收尾。另一個值得注意的點是FF_LBA64這個宏。exFAT 卷的容量可以做得很大當扇區(qū)數(shù)超過 32 位 LBA 能表達的范圍時就需要 64 位扇區(qū)尋址。R0.14b 對這部分的支持已經(jīng)穩(wěn)定如果你未來要接大容量 eMMC 或者高速 SDXC 卡這一步是繞不過去的。1.2 開源包打開之后先認識這幾個文件FATFS 不像 Linux VFS 那樣龐大它整個源碼包很小。解壓后主要文件就這幾個文件作用ff.cFAT/exFAT 核心邏輯所有文件操作 API 的實現(xiàn)ff.h公共頭文件定義 API、數(shù)據(jù)結構和錯誤碼ffconf.h配置頭文件所有裁剪選項都在這里diskio.h/diskio.c底層設備接口層由用戶編寫或移植ffunicode.cUnicode 和 OEM 代碼頁轉換表用于長文件名支持option/目錄可選的代碼頁和額外功能實現(xiàn)我建議第一次接觸時不要急著看ff.c里的實現(xiàn)細節(jié)而是先讀ff.h里的函數(shù)聲明和結構體定義再打開ffconf.h逐行看配置項。理解這層之后整個系統(tǒng)的輪廓就清晰了應用層調(diào)用 API核心層處理 FAT 表和目錄項設備層最終把扇區(qū)讀寫映射到 SD 卡、Flash 或者其他存儲介質(zhì)上。2. 源碼層級拆解應用層、核心層、設備層如何協(xié)作FATFS 的設計思路可以這樣理解它把文件系統(tǒng)分成了三個層次層與層之間用明確的接口隔開。上層是一個面向用戶的文件操作 API類似你熟悉的fopen、fread、fwrite中間是ff.c里的核心實現(xiàn)負責解析 FAT 表、維護目錄項、管理簇鏈底層是設備驅動接口處理真正和硬件打交道的扇區(qū)讀寫。2.1 應用層 API從 f_open 到 f_forward應用層的入口是f_open它的邏輯很像 C 語言標準庫的fopen指定一個路徑和訪問模式返回一個FIL文件對象。之后就可以通過f_read、f_write、f_lseek來讀寫文件最后用f_close關閉并釋放資源。R0.14b 里還提供了一些實用函數(shù)比如f_mkdir創(chuàng)建目錄、f_unlink刪除文件、f_rename重命名、f_stat獲取文件信息。比較容易被忽略的是f_mount。它負責注冊一個邏輯驅動器號對應的卷比如f_mount(fs, 0:, 1)表示把fs對象掛載到0:上最后一個參數(shù)為 1 時表示立即掛載為 0 時則延遲到第一次訪問文件時再掛載。很多人第一次使用 FATFS 時會忘記調(diào)用f_mount直接f_open結果返回FR_INVALID_DRIVE就是這個原因。這里有一個典型的理解偏差f_mount并不是把存儲介質(zhì)重新格式化它只是把一個文件系統(tǒng)對象和邏輯驅動器號綁定起來后續(xù)所有寫著0:/的文件操作都會走這個卷。如果你更換了 SD 卡需要先f_mount卸載舊卷再重新掛載新卷。2.2 設備層 diskio把控制權真正交給你的代碼設備層一共六個函數(shù)這是移植 FATFS 時唯一需要自己動手的地方DSTATUS disk_initialize(BYTE pdrv); DSTATUS disk_status(BYTE pdrv); DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count); DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count); DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff); DWORD get_fattime(void);disk_initialize初始化硬件disk_read和disk_write按扇區(qū)讀寫數(shù)據(jù)disk_ioctl處理一些控制命令比如GET_SECTOR_COUNT獲取總扇區(qū)數(shù)、GET_SECTOR_SIZE獲取扇區(qū)大小、CTRL_SYNC確保寫入落盤。get_fattime返回當前時間FATFS 會把它寫入文件的日期時間字段通常由 MCU 的 RTC 驅動提供。為什么這套設計被廣泛應用因為接口足夠簡單。你不需要理解文件系統(tǒng)的具體實現(xiàn)只要能把扇區(qū)讀出來、寫進去文件系統(tǒng)的事情就全交給ff.c。這就像你不需要懂圖書館怎么整理圖書只需要能從書架上取出指定編號的書再放回去圖書管理系統(tǒng)自然會維護好索引。2.3 配置層 ffconf.h裁剪才是精髓所在ffconf.h直接決定 FATFS 編譯出來有多大、功能有多少。我每次移植新工程第一件事就是打開這個文件逐項確認而不是用默認配置直接編譯。最常用的幾個選項FF_USE_LFN長文件名支持0 表示關閉1 或 2 表示啟用區(qū)別在于文件名緩沖區(qū)在內(nèi)部還是外部。FF_MAX_LFN最大長文件名長度默認 255。FF_FS_EXFAT是否支持 exFAT開啟后代碼體積會增大。FF_USE_MKFS是否啟用格式化功能格式化功能在量產(chǎn)寫卡時很實用。FF_FS_MINIMIZE裁剪 API 函數(shù)數(shù)量追求最小資源時可以關掉一部分高級函數(shù)。FF_FS_NORTC當沒有 RTC 時用它代替get_fattime文件時間戳會固定成某個值。這里我最后的建議是不用的功能堅決關掉。比如產(chǎn)品只讀數(shù)據(jù)不寫入就把FF_FS_READONLY打開ff.c里所有寫文件相關代碼都不會編譯進去Flash 占用能省很多。3. 在 STM32 上把 FATFS R0.14b 完整跑起來這一節(jié)講真刀真槍的移植。以 STM32F103 SD 卡為例我盡量把每個環(huán)節(jié)說清楚因為實際調(diào)試時最容易出問題的往往不是 FATFS 本身的邏輯而是底層驅動的對接。3.1 前置準備和最容易忽視的電平問題硬件上先把 SD 卡接好。如果用 SPI 方式驅動至少需要 MISO、MOSI、SCK、CS 四根信號線如果使用 SPI 模式的 SD 卡控制器的 SPI 速率最好在初始化階段設置得低一些很多卡在低速下才能穩(wěn)定握手。有些卡在較高 SPI 時鐘下初始化經(jīng)常返回超時這就是為什么初始化代碼里通常會先把波特率降到 400kHz 左右。電平問題很關鍵。SD 卡工作電壓是 2.7V 到 3.6V很多 STM32 開發(fā)板有板載電平轉換可以直接插卡。如果你是自己飛線連接一定要確認 MCU 的 GPIO 電平匹配否則會間歇性讀寫失敗甚至燒壞卡。3.2 五步完成移植流程第一步把官方源碼里的source目錄拷貝到工程里注意加入編譯路徑。第二步根據(jù)你的存儲介質(zhì)實現(xiàn)diskio.c里的六個函數(shù)。第三步打開ffconf.h根據(jù)項目需要配置宏。第四步編寫掛載和文件操作代碼。第五步燒錄后運行測試。以 SD 卡 SPI 模式為例disk_initialize里的動作大致是拉高 CS 延時若干毫秒、發(fā)送至少 74 個時鐘周期的空指令、發(fā)送 CMD0 進入 SPI 模式、發(fā)送 CMD1 或 CMD8ACMD41 完成初始化、讀取 CID/CSD 寄存器確認扇區(qū)數(shù)。disk_read發(fā)送 CMD17 讀單扇區(qū)或者 CMD18 讀多扇區(qū)disk_write發(fā)送 CMD24 寫單扇區(qū)或 CMD25 寫多扇區(qū)。這些屬于 SD 卡協(xié)議層的骨架搞過 SD 卡驅動的人應該很熟。3.3 第一個讀寫測試這樣寫掛載成功之后先做個最簡單的寫入測試FATFS fs; FIL fil; FRESULT res; UINT bw; res f_mount(fs, 0:, 1); // 掛載立即掛載 if (res FR_OK) { res f_open(fil, 0:/test.txt, FA_CREATE_ALWAYS | FA_WRITE); if (res FR_OK) { f_write(fil, hello fatfs\r\n, 13, bw); f_close(fil); } }如果這一步返回FR_OK說明 FATFS 的整個工作鏈路已經(jīng)通了。接下來可以調(diào)用f_mkfs做一次格式化再創(chuàng)建目錄、寫多個文件、讀回校驗基本就能確認文件系統(tǒng)穩(wěn)定。4. 使用 FATFS R0.14b 時最典型的幾類坑網(wǎng)上討論 FATFS 的帖子很多但真正有價值的往往不是怎么用而是為什么這樣用就出問題了。我把實際項目里遇到的高頻問題整理一下按排查鏈路來講。4.1 掛載失敗 FR_NO_FILESYSTEMf_mount后返回FR_NO_FILESYSTEM意思是這塊介質(zhì)上沒有有效的 FAT 引導扇區(qū)。排查順序是這樣第一步確認硬件有沒有問題。用示波器或邏輯分析儀看 SD 卡 SPI 總線有沒有數(shù)據(jù)如果disk_read讀出來的數(shù)據(jù)全是 0xFF說明通信根本沒有建立。第二步確認已經(jīng)調(diào)用了格式化。新買的 SD 卡雖然自帶 FAT32 格式但有些卡出廠是 raw 狀態(tài)插到設備上自然無法識別。第三步確認disk_ioctl里的GET_SECTOR_SIZE返回了正確值。很多 SPI Flash 的扇區(qū)大小是 4096 字節(jié)如果你直接按 512 字節(jié)處理FATFS 會把整個數(shù)據(jù)結構讀錯。如果是 SPI Flash 這一類非 SD 卡設備沒有現(xiàn)成的 FAT 文件系統(tǒng)必須先f_mkfs格式化。注意f_mkfs也有自己的參數(shù)包括扇區(qū)大小、分配單元和分區(qū)類型需要跟介質(zhì)特性匹配。4.2 長文件名亂碼和中文名問題FATFS 默認情況下長文件名是關掉的因為需要額外的內(nèi)存緩沖。如果你打開FF_USE_LFN后仍然出現(xiàn)亂碼先檢查代碼頁和編碼方式。FATFS 的文件名在內(nèi)部默認是 ASCII 和 OEM 代碼頁如果你想支持 UTF-8 中文需要設置FF_CODE_PAGE為對應代碼頁并在讀寫文件名時使用匹配的編碼。很多人的問題是PC 端用 Windows 創(chuàng)建了中文文件名的文件嵌入式設備卻以 UTF-8 去讀結果當然是亂碼。另外FF_MAX_LFN如果設置過小超出長度的文件名會被截斷或返回FR_INVALID_NAME。不要以為 255 是默認值就一定夠用有些工程的FF_MAX_LFN被裁剪成 64這時候放入長文件名就很容易被卡住。4.3 文件時間全部變成 1980 年如果創(chuàng)建的文件在 PC 上顯示時間是 1980-01-01說明get_fattime返回了 0或者你啟用了FF_FS_NORTC。FATFS 用這個時間戳更新目錄項里的日期時間字段如果你沒有實現(xiàn) RTC或者 RTC 剛上電還沒初始化好它就會把時間寫成 FAT 約定的最小值。對大多數(shù)記錄型設備來說這個不影響功能但如果產(chǎn)品需要上報文件創(chuàng)建時間就必須解決。4.4 寫入之后數(shù)據(jù)沒保存拔電后發(fā)現(xiàn)文件不對這是最坑的一類問題?,F(xiàn)象是程序里f_write返回成功斷電后重新上電文件內(nèi)容缺失或者文件損壞。原因通常是寫入的數(shù)據(jù)還在 FATFS 的緩沖區(qū)里沒有真正寫到 SD 卡。f_write只是把數(shù)據(jù)交給文件系統(tǒng)的內(nèi)部緩沖只有緩沖區(qū)滿了或者調(diào)用f_sync、f_close時才會把數(shù)據(jù)刷到底層設備。所以關鍵數(shù)據(jù)寫入后一定要調(diào)f_sync或者干脆寫完就f_close。f_close內(nèi)部會做兩件事刷新緩沖、更新目錄項。如果你在寫日志時希望每條數(shù)據(jù)都及時落盤就每寫幾條調(diào)用一次f_sync代價是寫入速度會明顯下降。5. 性能與可靠性怎么把 FATFS 調(diào)得更順手很多人以為 FATFS 慢是文件系統(tǒng)本身效率低其實多數(shù)情況下是底層驅動沒發(fā)揮好。文件系統(tǒng)層面能優(yōu)化的點也不少。5.1 多扇區(qū)讀寫比單扇區(qū)快得多FATFS 的disk_read和disk_write參數(shù)里帶了一個count表示扇區(qū)數(shù)。底層驅動完全可以把這些扇區(qū)一次性讀上來或寫下去而不是在文件系統(tǒng)里循環(huán)調(diào)用單扇區(qū)讀寫。對 SD 卡 SPI 模式來說多塊讀用 CMD18多塊寫用 CMD25配合CTRL_SYNC保證寫完成整體吞吐量可以比單扇區(qū)翻好幾倍。還有一點容易被忽略緩沖區(qū)必須對齊。在 Cortex-M 平臺或者開啟了 DMA 的場景里如果緩沖區(qū)的地址沒有按底層要求對齊DMA 傳輸可能直接 HardFault或者數(shù)據(jù)錯位。FATFS 文件對象里自帶一個扇區(qū)緩沖區(qū)如果你用的是FF_FS_TINY模式這個緩沖區(qū)會跟文件系統(tǒng)共享內(nèi)存節(jié)省但緩存命中率降低需要根據(jù)項目取舍。5.2 啟用 FASTSEEK 處理大文件隨機訪問FATFS 默認的f_lseek是線性查找簇鏈對順序寫入沒問題但如果你頻繁定位大文件的不同位置性能會很難看。解決辦法是開啟FF_USE_FASTSEEK在打開文件后分配一個clmtbl[]數(shù)組通過f_lseek預建映射表。這樣之后的隨機定位會快很多。代價是每個文件對象要多分配一塊內(nèi)存數(shù)組大小跟文件的總簇數(shù)有關。5.3 掉電安全和設備壽命要自己想清楚FATFS 本身沒有日志功能不像有些文件系統(tǒng)有 journal掉電時如果正好在寫 FAT 表就有可能造成目錄項損壞、文件系統(tǒng)丟失。這是 FAT 格式的先天限制不是版本 bug。要緩解掉電風險一是重要參數(shù)寫入后立刻f_sync二是盡量把日志寫入到一個固定文件里減少頻繁的目錄項更新。對于 SPI Flash還要額外考慮磨損均衡。FATFS 不管 Flash 的擦寫壽命如果你的數(shù)據(jù)變化頻繁同一個扇區(qū)反復擦寫可能很快耗盡壽命。這種情況下最好在底層加一層 Flash 轉換層或者用CTRL_TRIM配合存儲介質(zhì)特性。SD 卡一般自帶控制器做磨損均衡SPI Flash 沒有這個待遇。6. 看到新版本要不要升級R0.14b 之后FatFs 后續(xù)版本在不斷迭代。新版本確實修了很多邊角問題也補充了一些功能整體 API 變化不大老工程升級不算困難。但我的觀點是如果產(chǎn)品已經(jīng)穩(wěn)定跑在 R0.14b 上沒必要為了新去動底層存儲代碼。文件系統(tǒng)這種模塊升級引發(fā)的回歸風險往往大于功能收益。如果是新項目直接選用當時最新的穩(wěn)定版當然沒問題畢竟官方修復的 bug 和兼容性問題確實有價值。R0.14b 的意義在于它被大量項目驗證過網(wǎng)上關于它的問題答案幾乎都能找到遇到問題是最好排查的狀態(tài)。我個人在實際項目里的做法是把 FATFS 源碼固定在一個版本里鎖死作為基礎版本同時把底層驅動和配置文件單獨管理。這樣以后不管是升級主控型號還是更換存儲介質(zhì)文件系統(tǒng)層的改動都被限制在可控范圍內(nèi)。存儲這一塊求穩(wěn)永遠比求新重要。本文還有配套的精品資源點擊獲取