據(jù)落盤實(shí)戰(zhàn):fatfs移植與CSV文件生成)
簡(jiǎn)介這份資源面向嵌入式開(kāi)發(fā)學(xué)習(xí)者與STM32工程師聚焦在STM32F429平臺(tái)上集成FatFS文件系統(tǒng)、驅(qū)動(dòng)SD卡寫入數(shù)據(jù)并生成.csv文件同時(shí)結(jié)合以太網(wǎng)驅(qū)動(dòng)搭建TCP服務(wù)器接收網(wǎng)絡(luò)數(shù)據(jù)。內(nèi)容覆蓋FatFS的移植與參數(shù)配置、SDIO或SPI方式下的SD卡初始化與讀寫、CSV格式數(shù)據(jù)的文件創(chuàng)建與寫入以及基于lwIP的TCP服務(wù)器連接建立、數(shù)據(jù)接收與處理流程適合具備一定C語(yǔ)言與嵌入式基礎(chǔ)、希望打通存儲(chǔ)與網(wǎng)絡(luò)通信鏈路的開(kāi)發(fā)者參考。資源包共1482個(gè)文件以629個(gè)c源文件、382個(gè)h頭文件為主另含144個(gè)d依賴文件、144個(gè)o目標(biāo)文件、142個(gè)crf交叉引用文件及少量庫(kù)文件、工程配置與說(shuō)明文檔壓縮包約59.43MB工程結(jié)構(gòu)完整便于直接編譯與二次開(kāi)發(fā)。目前已有8150人學(xué)習(xí)下載可幫助讀者快速理解SD卡文件系統(tǒng)與TCP數(shù)據(jù)落盤的完整實(shí)現(xiàn)思路。1. 從一塊 SD 卡說(shuō)起為什么 fatfs 是 STM32 數(shù)據(jù)落盤繞不開(kāi)的一環(huán)手上有個(gè) STM32 的采集板傳感器數(shù)據(jù)在 RAM 里滾了幾百個(gè)字節(jié)一斷電全沒(méi)了。想把數(shù)據(jù)存下來(lái)最直接的辦法就是插一張 SD 卡讓它像 U 盤一樣能被電腦讀走。但 STM32 本身不認(rèn)識(shí) FAT32 文件系統(tǒng)它只會(huì)按扇區(qū)讀寫原始數(shù)據(jù)。這時(shí)候 fatfs 就派上用場(chǎng)了——它是一個(gè)輕量級(jí)的 FAT 文件系統(tǒng)模塊專門給單片機(jī)用能把裸扇區(qū)操作封裝成f_open、f_write、f_close這種文件級(jí)接口。這個(gè)資源做的事很明確在 STM32 上通過(guò) SDIO 或 SPI 驅(qū)動(dòng) SD 卡移植 fatfs然后周期性地把采集數(shù)據(jù)寫成.csv文件。csv 的好處是電腦端 Excel、Python pandas 直接能打開(kāi)不用自己寫解析器。適合誰(shuí)做數(shù)據(jù)采集、工業(yè)記錄儀、離線日志存儲(chǔ)的嵌入式開(kāi)發(fā)者。如果你正在糾結(jié)數(shù)據(jù)怎么落盤、csv 怎么生成、fatfs 怎么配這篇可以跟著走一遍。2. fatfs 移植與 SD 卡底層驅(qū)動(dòng)從扇區(qū)到文件句柄2.1 先搞清楚 fatfs 的分層結(jié)構(gòu)fatfs 的設(shè)計(jì)是分層解耦的。最上層是ff.c提供f_open、f_read、f_write這些 API跟具體硬件無(wú)關(guān)。中間是diskio.c這是一層薄薄的適配層把 fatfs 的disk_read、disk_write、disk_initialize映射到你自己的 SD 卡驅(qū)動(dòng)。最底下才是 SDIO 或 SPI 的寄存器操作。常見(jiàn)做法是SDIO 模式跑 4 位總線理論速率能到幾十 Mbps適合高速采集SPI 模式接線簡(jiǎn)單但速率受限于 SPI 時(shí)鐘一般也就幾 Mbps。我一般優(yōu)先選 SDIO除非引腳實(shí)在不夠。移植的核心工作就三件事實(shí)現(xiàn)diskio.c里的五個(gè)函數(shù)、配置ffconf.h里的宏、確保 SD 卡驅(qū)動(dòng)能正常初始化并讀寫扇區(qū)。2.2 diskio.c 的五個(gè)函數(shù)怎么寫diskio.c里必須實(shí)現(xiàn)disk_initialize、disk_status、disk_read、disk_write、disk_ioctl。前兩個(gè)是狀態(tài)查詢后三個(gè)是實(shí)際讀寫。下面是一個(gè)基于 SDIO 的典型實(shí)現(xiàn)骨架#include diskio.h #include sdio_driver.h // 你自己的 SDIO 驅(qū)動(dòng)頭文件 #define DEV_SD 0 DSTATUS disk_initialize(BYTE pdrv) { if (pdrv ! DEV_SD) return RES_PARERR; if (SDIO_Init() ! 0) return STA_NOINIT; // 初始化失敗 return 0; // 成功返回 0 } DSTATUS disk_status(BYTE pdrv) { if (pdrv ! DEV_SD) return RES_PARERR; return 0; // 簡(jiǎn)化處理實(shí)際可查卡是否插入 } DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { if (pdrv ! DEV_SD) return RES_PARERR; if (SDIO_ReadBlocks((uint8_t *)buff, sector, count) ! 0) return RES_ERROR; return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) { if (pdrv ! DEV_SD) return RES_PARERR; if (SDIO_WriteBlocks((uint8_t *)buff, sector, count) ! 0) return RES_ERROR; return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { if (pdrv ! DEV_SD) return RES_PARERR; switch (cmd) { case CTRL_SYNC: return RES_OK; // 等待寫完成SDIO 阻塞模式下可直接返回 case GET_SECTOR_COUNT: *(DWORD *)buff SDIO_GetSectorCount(); return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff 1; // 擦除塊大小SD 卡填 1 即可 return RES_OK; default: return RES_PARERR; } }邏輯說(shuō)明disk_initialize里調(diào)用你的 SDIO 初始化函數(shù)返回 0 表示成功。disk_read和disk_write直接轉(zhuǎn)發(fā)給底層塊讀寫函數(shù)注意sector是扇區(qū)號(hào)不是字節(jié)地址。disk_ioctl里GET_SECTOR_COUNT必須返回正確的扇區(qū)總數(shù)否則 fatfs 格式化或掛載時(shí)會(huì)算錯(cuò)容量。參數(shù)說(shuō)明pdrv是物理驅(qū)動(dòng)號(hào)單卡場(chǎng)景固定為 0。buff是數(shù)據(jù)緩沖區(qū)count是扇區(qū)數(shù)。CTRL_SYNC在阻塞寫模式下可以直接返回 OK但如果你用了 DMA 或非阻塞寫這里必須等寫完成。2.3 ffconf.h 里必須改的幾個(gè)宏ffconf.h是 fatfs 的配置文件默認(rèn)值不一定適合 STM32。下面幾個(gè)宏我每次都會(huì)檢查宏名默認(rèn)值建議值原因_FS_TINY00設(shè)為 1 會(huì)省 RAM 但性能下降采集場(chǎng)景不差這點(diǎn)內(nèi)存_FS_READONLY00要寫 csv 必須為 0_USE_MKFS01卡沒(méi)格式化時(shí)可以直接在板子上格式化_CODE_PAGE932936932 是日文936 是簡(jiǎn)體中文避免文件名亂碼_USE_LFN01 或 2支持長(zhǎng)文件名csv 文件名可以寫完整日期_MAX_SS512512扇區(qū)大小SD 卡固定 512_VOLUMES11單卡場(chǎng)景一個(gè)卷夠了_USE_LFN設(shè)為 1 時(shí)用靜態(tài)緩沖區(qū)設(shè)為 2 時(shí)用棧上動(dòng)態(tài)分配設(shè)為 3 時(shí)用堆。STM32 上我一般用 1省得折騰內(nèi)存管理。2.4 掛載與格式化第一次用卡必須做的事新卡或者電腦上格式化過(guò)的卡掛載前最好先確認(rèn)文件系統(tǒng)類型。如果卡是 exFAT 或 NTFSfatfs 默認(rèn)不支持需要f_mkfs重新格式化成 FAT32。下面是一段掛載加自動(dòng)格式化的代碼FATFS fs; // 文件系統(tǒng)對(duì)象 FIL fil; // 文件對(duì)象 FRESULT fres; fres f_mount(fs, 0:, 1); // 1 表示立即掛載 if (fres ! FR_OK) { // 掛載失敗嘗試格式化 fres f_mkfs(0:, FM_FAT32, 0, NULL, 0); if (fres ! FR_OK) { printf(Format failed: %d\r\n, fres); return; } fres f_mount(fs, 0:, 1); if (fres ! FR_OK) { printf(Mount after format failed: %d\r\n, fres); return; } }邏輯說(shuō)明f_mount的第三個(gè)參數(shù)為 1 時(shí)會(huì)立即執(zhí)行掛載操作為 0 時(shí)只注冊(cè)不掛載。f_mkfs的第二個(gè)參數(shù)指定格式類型FM_FAT32適合 32GB 以下的卡。第四個(gè)參數(shù)是工作緩沖區(qū)傳 NULL 時(shí) fatfs 會(huì)用內(nèi)部緩沖區(qū)但有些版本要求外部提供傳 NULL 前先確認(rèn)版本。參數(shù)說(shuō)明0:是邏輯驅(qū)動(dòng)器號(hào)對(duì)應(yīng)diskio.c里的DEV_SD。f_mkfs的第三個(gè)參數(shù)是簇大小傳 0 表示自動(dòng)選擇。3. 生成 csv 文件從 f_open 到 f_printf 的完整鏈路3.1 csv 文件的寫入流程csv 本質(zhì)是純文本每行一條記錄字段用逗號(hào)分隔。fatfs 提供f_printf函數(shù)可以直接往文件里寫格式化字符串比手動(dòng)拼接再f_write方便得多。但f_printf需要開(kāi)啟_FS_READONLY為 0 且_USE_STRFUNC不為 0。典型流程是打開(kāi)文件追加模式→ 寫表頭如果是新文件→ 寫數(shù)據(jù)行 → 關(guān)閉文件。下面是一個(gè)完整的寫入函數(shù)FRESULT write_csv_record(FIL *fil, uint32_t timestamp, float temp, float humi) { FRESULT fres; UINT bw; // 以追加模式打開(kāi)如果文件不存在則創(chuàng)建 fres f_open(fil, 0:data.csv, FA_OPEN_ALWAYS | FA_WRITE); if (fres ! FR_OK) return fres; // 移動(dòng)到文件末尾 fres f_lseek(fil, f_size(fil)); if (fres ! FR_OK) { f_close(fil); return fres; } // 如果文件為空先寫表頭 if (f_size(fil) 0) { f_printf(fil, timestamp,temperature,humidity\r\n); } // 寫數(shù)據(jù)行 f_printf(fil, %lu,%.2f,%.2f\r\n, timestamp, temp, humi); f_close(fil); // 必須關(guān)閉否則數(shù)據(jù)可能還在緩存里 return FR_OK; }邏輯說(shuō)明FA_OPEN_ALWAYS表示文件存在就打開(kāi)不存在就創(chuàng)建。f_lseek移動(dòng)到末尾是為了追加而不是覆蓋。f_size返回當(dāng)前文件大小用來(lái)判斷是否需要寫表頭。f_printf的格式化語(yǔ)法和標(biāo)準(zhǔn) C 的printf基本一致但浮點(diǎn)數(shù)支持取決于_USE_STRFUNC的配置。參數(shù)說(shuō)明fil是文件對(duì)象指針調(diào)用前需要先f(wàn)_mount。timestamp用%lu因?yàn)樗?32 位無(wú)符號(hào)數(shù)。%.2f保留兩位小數(shù)csv 里夠用了。\r\n是 Windows 換行符Excel 打開(kāi)不會(huì)亂行。3.2 寫入頻率與緩沖策略每次采集都f_openf_close一次在低頻場(chǎng)景比如每秒一條沒(méi)問(wèn)題。但如果每秒要寫幾十上百條頻繁開(kāi)關(guān)文件會(huì)拖慢速度因?yàn)槊看蝔_close都會(huì)觸發(fā) FAT 表更新和扇區(qū)寫入。我一般會(huì)這樣做文件保持打開(kāi)狀態(tài)每隔 N 條或者每隔幾秒調(diào)用一次f_sync把緩存刷到卡里。f_sync比f(wàn)_close輕量只刷新數(shù)據(jù)不關(guān)閉文件。掉電風(fēng)險(xiǎn)高的場(chǎng)景N 取小一點(diǎn)比如 10 條同步一次對(duì)掉電不敏感的場(chǎng)景可以 100 條同步一次。// 保持文件打開(kāi)定期 sync static FIL csv_fil; static uint32_t record_count 0; void csv_append(uint32_t ts, float t, float h) { f_printf(csv_fil, %lu,%.2f,%.2f\r\n, ts, t, h); record_count; if (record_count % 10 0) { f_sync(csv_fil); // 每 10 條刷一次 } }邏輯說(shuō)明f_sync會(huì)把文件緩存區(qū)的臟數(shù)據(jù)寫回 SD 卡但不關(guān)閉文件句柄。這樣既減少了開(kāi)關(guān)開(kāi)銷又控制了掉電丟數(shù)據(jù)的窗口。參數(shù)說(shuō)明record_count是靜態(tài)變量記錄自上次 sync 以來(lái)寫了多少條。取 10 是經(jīng)驗(yàn)值具體看你的數(shù)據(jù)重要程度和 SD 卡寫入速度。3.3 文件名帶時(shí)間戳避免覆蓋和歷史追溯固定叫data.csv有個(gè)問(wèn)題每次上電都往同一個(gè)文件追加時(shí)間長(zhǎng)了文件巨大而且分不清哪次采集是哪次。更好的做法是用 RTC 的時(shí)間生成文件名比如20250101_120000.csv。這樣每次上電新建一個(gè)文件電腦上按文件名就能排序。void create_csv_by_time(FIL *fil, RTC_TimeTypeDef *time, RTC_DateTypeDef *date) { char fname[32]; snprintf(fname, sizeof(fname), 0:%04d%02d%02d_%02d%02d%02d.csv, date-Year 2000, date-Month, date-Date, time-Hours, time-Minutes, time-Seconds); f_open(fil, fname, FA_CREATE_ALWAYS | FA_WRITE); }邏輯說(shuō)明snprintf拼出文件名FA_CREATE_ALWAYS表示每次都新建如果同名文件存在就覆蓋。RTC 的年份寄存器通常存的是 2000 年起的偏移所以要加 2000。參數(shù)說(shuō)明fname緩沖區(qū)至少 32 字節(jié)0:前綴不能少。snprintf比sprintf安全能防止緩沖區(qū)溢出。4. 避坑與排查SD 卡 fatfs 最容易翻車的五個(gè)地方4.1 掛載返回 FR_NOT_READY 或 FR_DISK_ERR現(xiàn)象f_mount返回FR_NOT_READY或者f_open返回FR_DISK_ERR。原因九成是disk_initialize沒(méi)成功或者disk_read讀不到扇區(qū) 0。SD 卡上電后需要一段初始化時(shí)間如果 MCU 啟動(dòng)太快卡還沒(méi)準(zhǔn)備好就發(fā)命令就會(huì)失敗。解決在disk_initialize里加延時(shí)上電后至少等 1ms 再發(fā) CMD0。另外檢查 SDIO 時(shí)鐘分頻初始化階段必須降到 400kHz 以下初始化完成后再切到高速。SPI 模式同理先低速再高速。4.2 寫 csv 后電腦打開(kāi)亂碼或列錯(cuò)位現(xiàn)象csv 文件在 Excel 里打開(kāi)中文亂碼或者所有數(shù)據(jù)擠在一列。原因Excel 默認(rèn)按系統(tǒng)區(qū)域設(shè)置解析 csv如果文件里沒(méi)有 BOM中文可能亂碼。列錯(cuò)位通常是分隔符問(wèn)題有些區(qū)域設(shè)置用分號(hào)而不是逗號(hào)。解決表頭和數(shù)據(jù)里不要用中文字段名用英文。如果一定要中文在文件開(kāi)頭寫入 UTF-8 BOM0xEF 0xBB 0xBF。列錯(cuò)位的話確認(rèn)f_printf里用的是逗號(hào)且字段值里沒(méi)有多余的逗號(hào)。4.3 f_write 返回成功但數(shù)據(jù)沒(méi)落盤現(xiàn)象f_write和f_close都返回FR_OK但拔卡插電腦上發(fā)現(xiàn)文件是空的或者少了最后幾條。原因fatfs 有緩存機(jī)制f_write只是寫到緩存f_close才真正刷到卡。如果f_close之前掉電緩存里的數(shù)據(jù)就丟了。另外有些 SD 卡內(nèi)部也有緩存f_close返回后卡可能還在寫。解決對(duì)掉電敏感的場(chǎng)景每次寫完調(diào)用f_sync并且f_sync之后加一句disk_ioctl(0, CTRL_SYNC, NULL)確??▋?nèi)部也寫完。如果還不行換一張卡試試有些雜牌卡緩存策略很激進(jìn)。4.4 長(zhǎng)時(shí)間寫入后速度越來(lái)越慢現(xiàn)象剛開(kāi)始寫 csv 很快寫了幾千條之后越來(lái)越慢甚至卡死。原因FAT 表碎片化。頻繁創(chuàng)建刪除文件、或者文件反復(fù)增長(zhǎng)會(huì)導(dǎo)致 FAT 表里簇鏈變得很長(zhǎng)每次寫都要遍歷簇鏈。另外如果 SD 卡質(zhì)量差壞塊重映射也會(huì)拖慢速度。解決定期整理比如每天新建一個(gè)文件而不是往同一個(gè)文件無(wú)限追加。格式化時(shí)選大一點(diǎn)的簇比如 32KB減少簇鏈長(zhǎng)度。如果卡已經(jīng)用了很久用f_mkfs重新格式化一次。4.5 多任務(wù)環(huán)境下 fatfs 重入問(wèn)題現(xiàn)象在 RTOS 里一個(gè)任務(wù)寫 csv另一個(gè)任務(wù)讀配置偶爾會(huì)死機(jī)或者文件損壞。原因fatfs 默認(rèn)不是線程安全的多個(gè)任務(wù)同時(shí)調(diào)用f_open、f_write會(huì)破壞內(nèi)部狀態(tài)。解決給 fatfs 加互斥鎖。簡(jiǎn)單做法是用一個(gè)全局標(biāo)志位訪問(wèn)前檢查并置位訪問(wèn)完清除。更規(guī)范的做法是用 RTOS 的互斥量在f_mount之后創(chuàng)建每次文件操作前 take操作完 give。另外_FS_REENTRANT宏可以開(kāi)啟 fatfs 自帶的重入保護(hù)但需要你實(shí)現(xiàn)ff_req_grant和ff_rel_grant兩個(gè)信號(hào)量函數(shù)。5. 進(jìn)階技巧用 f_printf 直接輸出帶表頭的結(jié)構(gòu)化 csv前面寫的 csv 是手動(dòng)拼表頭和數(shù)據(jù)行其實(shí) fatfs 的f_printf可以更靈活。比如你要記錄多種傳感器字段數(shù)量不固定可以用一個(gè)格式化函數(shù)統(tǒng)一處理。下面這個(gè)技巧我經(jīng)常用把表頭和數(shù)據(jù)行都封裝成函數(shù)字段用數(shù)組傳入避免每次改字段都要改代碼。// 寫表頭fields 是字段名數(shù)組count 是字段數(shù) FRESULT csv_write_header(FIL *fil, const char **fields, uint8_t count) { for (uint8_t i 0; i count; i) { f_printf(fil, %s, fields[i]); if (i count - 1) f_printf(fil, ,); } f_printf(fil, \r\n); return FR_OK; } // 寫一行 float 數(shù)據(jù) FRESULT csv_write_row(FIL *fil, const float *values, uint8_t count) { for (uint8_t i 0; i count; i) { f_printf(fil, %.3f, values[i]); if (i count - 1) f_printf(fil, ,); } f_printf(fil, \r\n); return FR_OK; }邏輯說(shuō)明csv_write_header遍歷字段名數(shù)組用逗號(hào)分隔最后換行。csv_write_row同理但輸出的是浮點(diǎn)數(shù)。這樣字段增減只需要改數(shù)組不用動(dòng)寫入邏輯。參數(shù)說(shuō)明fields是字符串指針數(shù)組values是浮點(diǎn)數(shù)組count是字段數(shù)量。%.3f保留三位小數(shù)按需調(diào)整。驗(yàn)證方法寫完之后把卡拔下來(lái)插電腦用 Excel 或文本編輯器打開(kāi)確認(rèn)列對(duì)齊、行數(shù)正確、沒(méi)有多余空行。更嚴(yán)格的驗(yàn)證是用 Python 的csv模塊讀一遍import csv with open(data.csv, r) as f: reader csv.reader(f) for row in reader: print(row)如果 Python 能正常解析說(shuō)明格式?jīng)]問(wèn)題。我一般還會(huì)檢查文件大小和記錄數(shù)是否匹配比如每條記錄約 30 字節(jié)1000 條就是 30KB 左右差太多說(shuō)明有丟數(shù)據(jù)。從那以后我每次寫完 csv 都強(qiáng)制走一遍「拔卡→電腦讀→Python 解析」的流程確認(rèn)無(wú)誤再批量部署。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取