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