ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

HC32F460外挂SD卡存储:FatFS移植与SPI模式实践

HC32F460外挂SD卡存储:FatFS移植与SPI模式实践 简介基于华大半导体HC32F460微控制器的SD卡文件系统移植资源面向嵌入式开发与物联网存储场景解决在MCU上通过FATFS高效读写SD卡的问题。资源共350个文件压缩包约15.92MB以C源码与头文件为主94个c、121个h同时包含IAR、Keil工程文件ewp/eww/uvprojx、链接脚本、编译中间文件o/d/crf及烧录算法flm便于在多种IDE中直接编译与调试。配套示例覆盖SD卡多线模式与DMA读写如1bit/4bit不同速率下的读写测试并集成了中文长文件名支持cc936/cc949/cc950可直接应用于数据日志或配置文件场景。项目采用mcu/bsp/driver/example等模块化结构清晰展示FATFS从底层驱动到应用接口的移植过程。目前已有301人学习该资源适合需要快速在HC32F460上实现文件系统的开发者参考。 我最近在调HC32F460的数据记录功能板子上的内部Flash实在不够用得外挂存储。翻了一圈方案最后还是老老实实走了SD卡加FatFS这条老路。SD卡便宜、容量大、拆下来插读卡器就能在电脑上读数据FAT文件系统又成熟稳定就算拔卡的时候没来得及安全弹出大部分情况下也能自己恢复或者直接重新格式化。整个过程踩了不少坑从SD卡上电时序到FatFS移植再到HC32F460的SRAM被吃干抹净写出来给后面做同类项目的朋友做个参考。1. 为什么选了SD卡SPI模式而不是SDIO以及FatFS的定位先说选型。HC32F460这颗芯片其实是带SDIO外设的理论上SDIO模式的读写吞吐量能到几十MB/s远不是SPI模式能比的。但我最后还是选了SPI模式核心原因有三个引脚占用少SDIO至少要6根线CLK、CMD、DAT0~DAT3而SPI模式只需要4根线SCK、MOSI、MISO、CS。代码复杂度低SDIO模式底层驱动要处理命令队列、中断/DMA、数据流控制出错排查难度高SPI模式就是把SD卡当成一个SPI从设备收发都是字节流逻辑简单直白。我的应用场景是周期性数据记录每秒写几百字节到几KBSPI模式的实际速度完全够用。如果你的项目需要在短时间内连续写入大文件比如音频采样或者视频流那就别折腾SPI模式了直接切SDIO加DMA。HC32F460的SDIO外设性能不差配合FatFS同样能跑只是驱动部分的调试工作量会翻倍。FatFS的选择几乎没什么争议。它是面向小型嵌入式系统的通用FAT/exFAT文件系统完全开源的C代码支持FAT12/16/32移植只需要提供底层磁盘读写接口代码量不会让你的工程膨胀太多。相比之下其他文件系统方案要么太重比如Linux的VFS那种要么不够通用。FatFS的接口设计很清晰应用层调用f_open/f_read/f_write/f_close操作文件底层通过disk_read/disk_write/disk_ioctl访问硬件中间层逻辑完全不用你管。2. SD卡SPI模式的硬件连接与初始化时序2.1 引脚连接与硬件注意事项SD卡在SPI模式下实际上是把MMC/SD协议的命令和数据通道通过SPI帧格式来承载所以硬件上只要把SD卡的对应引脚接到MCU的SPI接口上就行。我用的TF卡座是自弹式的引脚定义比较标准。SD卡引脚功能连接目标CS片选HC32F460 SPI片选引脚示例PA15SCK时钟HC32F460 SPI时钟引脚示例PA13MOSI主机输出从机输入HC32F460 SPI MOSI示例PA14MISO主机输入从机输出HC32F460 SPI MISO示例PA14旁边的对应引脚GND地公共地VCC电源3.3V有两点硬件上必须注意。第一SD卡SPI模式不支持热插拔如果你需要在运行中拔插SD卡一定要加卡检测引脚并且在软件里对卡状态进行轮询或者外部中断处理。我在原型阶段就直接固定卡不拔所以没做这一步。第二SD卡的电源引脚旁边要放一个10uF左右的去耦电容不要省否则高速读写时电压跌落会导致卡直接返回CRC错误或者命令超时。2.2 SPI外设参数配置HC32F460的SPI外设配置不算复杂但有些细节特别容易踩坑。我用了SPI1外设主模式8位数据宽度MSB先行时钟极性和相位都是0也就是CPOL0、CPHA0。这套参数是SPI模式SD卡的标准要求改错了卡根本不会响应。时钟频率方面SD卡上电后先要用100kHz到400kHz的低速时钟完成初始化等进入数据传输状态后再把速度提上去。HC32F460的SPI时钟源来自PCLK通过SPIx_CR寄存器的分频位控制。我芯片内部PCLK1跑50MHz初始化阶段配了128分频约390kHz后面读取OCR寄存器确认支持高电压范围之后再切到8分频约6.25MHz来读写数据。这里给一段初始化SPI的参考片段基于HC32F460的官方驱动库void SPI1_Init(uint32_t baud_div) { stc_spi_init_t stcSpiInit; memcpy(stcSpiInit, SPI1_DefaultInit, sizeof(stc_spi_init_t)); stcSpiInit.enClkPolarity SpiClkLowIdle; // CPOL0 stcSpiInit.enClkPhase SpiClkOddSample; // CPHA0 stcSpiInit.enDataMode SpiData8Bit; stcSpiInit.enTransMode SpiTransMsbFirst; stcSpiInit.enWorkMode SpiWorkMaster; SPI_Init(SPI1, stcSpiInit); SPI_SetBaudDiv(SPI1, baud_div); // 慢速初始化用大分频 SPI_Enable(SPI1); }2.3 SD卡命令发送与初始化序列SD卡初始化的核心逻辑其实很固定。SD卡在SPI模式下所有命令都是48位格式1位起始位0、1位传输位1、6位命令索引、32位参数、7位CRC和1位停止位。SPI模式下卡对CRC不校验但CMD0和CMD8这两个命令的CRC字段必须计算正确因为早期卡在进入SPI模式前仍在SD模式会校验CRC。标准初始化序列我按这个顺序来先给CS拉高然后连续发送至少74个时钟周期的0xFF让SD卡内部的供电稳定、同步完成。这一步最容易漏漏了之后CMD0永远得不到0x01响应。CS拉低发送CMD0参数0等待响应0x01说明卡已进入SPI模式。发送CMD8参数0x000001AA读取卡是否支持SDHC/SDXC。如果不支持返回0x01卡是SDSC v1.x如果返回0x05说明是MMC或者老卡识别会困难一点。循环发送ACMD41参数0x40000000直到返回0x00。ACMD41前必须先发CMD55这是SD卡协议的规定。读取OCR寄存器CMD58根据bit30判断卡是SDHC还是SDSC。如果是SDHC后面读写地址直接按块单位处理如果是SDSC地址是字节地址需要换算。发送CMD16设置块长度为512字节SDHC卡这一步其实可以省略但为了兼容性留着。这里有一段简化版的初始化代码实际工程里建议把超时机制加上uint8_t SD_Init(void) { uint8_t resp; uint32_t i; SD_CS_HIGH(); for (i 0; i 80; i) { SD_SPI_RW(0xFF); // 发送至少80个时钟 } SD_CS_LOW(); resp SD_SendCmd(CMD0, 0x00000000, 0x95); if (resp ! 0x01) return 1; resp SD_SendCmd(CMD8, 0x000001AA, 0x87); if (resp 0x01) { for (i 0; i 1000; i) { resp SD_SendCmd(CMD55, 0x00000000, 0x01); resp SD_SendCmd(ACMD41, 0x40000000, 0x01); if (resp 0x00) break; } if (resp ! 0x00) return 2; } resp SD_SendCmd(CMD58, 0x00000000, 0x01); if (resp 0x00) { // 读OCR寄存器判断SDHC } return 0; }命令响应读取有个细节SD卡返回响应前会有一个或多个0xFF的字节缓冲必须在发送完命令后继续读MISO直到读到非0xFF字节。很多人第一次调的时候直接发送命令后立刻读响应结果读到的是0xFF误以为卡没响应。我在这里折腾了整整一个晚上。3. FatFS的移植要点与diskio接口实现3.1 整体架构与文件布局FatFS的源码结构分三层应用层ff.c、ff.h、ffconf.h实现FAT文件系统的所有逻辑。中间层diskio.c、diskio.h实现四个底层接口函数。硬件层你自己的SPI驱动和SD卡命令函数。移植工作其实就是把ffconf.h的配置项按需改好再把diskio.c的函数实现填完整。理解这个数据流向很重要应用发起文件读写请求FatFS解析FAT表、目录项然后按逻辑扇区号LBA调用底层接口底层接口再把LBA转成SD卡命令通过SPI读写物理扇区。3.2 diskio接口函数详解diskio.c需要实现6个函数DSTATUS disk_status(BYTE pdrv); // 获取磁盘状态简单返回0表示正常 DSTATUS disk_initialize(BYTE pdrv); // 初始化磁盘这里调用SD_Init 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_read和disk_write是最核心的可以直接调用SD卡的单块读CMD17和单块写CMD24。如果FatFS配置里FF_USE_MULTI开启底层会传入count 1这时可以用多块命令CMD18/CMD25来提升效率。我在初版实现里为了验证逻辑先只做了单块读写让count循环执行再返回RES_OK功能完全正常只是速度慢。后面优化再改成多块。关键点SD卡的LBA地址是扇区的概念而FatFS传给底层的是相对偏移的扇区号。SD卡的寄存器地址直接按512字节对齐的扇区号来用即可不需要再乘以512除非SDSC模式是字节寻址那就必须block_lba * 512。disk_ioctl里有几个FatFS会查询的常量命令必须实现CTRL_SYNC确认所有写缓冲区的数据已经刷入SD卡。这个很关键一定要等SD卡不再处于忙状态返回RES_OK。GET_SECTOR_COUNT返回SD卡的总扇区数FatFS挂载和格式化时要用来计算容量。GET_BLOCK_SIZE返回擦除块大小通常返回1或者SD卡CSD里的擦除块大小。get_fattime如果返回一个固定日期也能跑但文件时间戳会永远是那个日期。我自己加了个钩子从HC32F460的RTC读时间再拼成FatFS要求的格式年-7bit月-4bit日-5bit时-5bit分-6bit秒-5bit。3.3 ffconf.h关键配置项ffconf.h的配置直接影响功能、内存和Flash占用。我按项目需求逐一过了一遍下面几个重点说明配置项我的取值说明FF_USE_LFN2启用长文件名使用栈上缓冲区。改成0则只支持8.3短文件名会损失可读性FF_VOLUMES1只挂载一张卡FF_MIN_SS/FF_MAX_SS512/512强制固定扇区大小这个很省内存见下节FF_USE_MKFS1需要f_mkfs做格式化FF_USE_STRFUNC1支持f_printf格式化写入FF_FS_LOCK2最多同时打开2个文件务必把FF_MIN_SS和FF_MAX_SS同时设成512。默认值里FF_MAX_SS4096FatFS会用FF_MAX_SS给文件对象分配扇区对齐缓冲区这会直接吃掉4KB内存在单片机上很肉痛。SD卡的物理扇区基本都是512字节直接锁死512没毛病。4. HC32F460的内存占用实测与优化取舍4.1 内存占用分析HC32F460根据型号不同SRAM从几十KB到上百KB不等。我用的是192KB SRAM的型号看起来挺充裕但实际把FatFS用起来才发现内存消耗远比想象中猛。做一个完整的文件读取/写入任务内存开销主要来自一个FIL文件对象FF_USE_LFN2、FF_MAX_SS512时FIL结构体大概占550字节左右如果FF_MAX_SS还是默认的4096FIL会直接涨到4KB以上。一个DIR目录对象约50到100字节。FatFS内部的路径缓冲默认PATH_MAX_DEPTH是8层每层12字节大约96字节。你在应用层分配的读写缓冲区比如f_read时传入的buff这个是自己控制的。我开了2KB的缓冲区做批量读写。SPI DMA缓冲区如果你用了DMA。综合下来一套基础FatFS读写环境大约占用2.5KB到3KB SRAM加上应用层缓冲和栈开销总共5KB左右其实完全可以接受。问题出在如果你不改FF_MAX_SS这个数字会暴涨到8KB以上。我实际测试时的内存占用参考表配置组合FIL内部缓冲估算总SRAM增量含读写缓冲LFN0, MAX_SS512约50字节约2.2KBLFN2, MAX_SS512约550字节约2.7KBLFN2, MAX_SS4096约4.6KB约6.8KBLFN3, MAX_SS512动态分配约550字节 堆需求取决于堆配置如果你的工程里还跑着RTOS、协议栈、GUI这些大户内存规划就要精打细算。我强烈建议在开发阶段就把SRAM使用量打印出来实测值永远比估算可靠。HC32F460的官方库或者常规嵌入式调试手段都能拿到栈高水位和堆剩余量。4.2 栈空间与中断嵌套问题FatFS本身是单线程设计的如果在RTOS环境多任务并发访问同一个卷必须在ffconf.h里开启FF_FS_REENTRANT给每个卷配一个互斥锁否则FAT表会被并发读写搞坏。栈方面FatFS的函数调用链比较深f_write-move_window/sync_window-disk_write-SD_WriteBlock-SPI_WriteData。每一层都有局部变量在Cortex-M4上走下来单次调用的栈峰值大约在400到600字节。我分配的任务栈是2KB留足了余量。如果不用RTOS、直接在裸机上跑中断嵌套就要注意了——避免在FATFS操作中插入长时间中断尤其是SD卡的写忙等待否则时间确定性会很差。4.3 一个让我数据损坏的失误我在调通过之后为了验证长时间写入稳定性做了一个每秒写一条日志的压力测试。跑了几分钟一切正常然后我发现上位机读出来的文件最后几条数据是乱的。排查下来问题出在我在f_write之后没有及时调用f_sync掉电时缓冲区的数据还没落盘。FatFS是带写缓冲的f_write只把数据写到内部缓冲区真正写SD卡要等缓冲区满、显式f_sync、或者f_close。对日志型应用来说每写一条记录就f_sync一次性能会有损失但换来了掉电安全性。后来我采取的策略是每写10条记录调一次f_sync同时利用HC32F460的低电压检测在掉电瞬间抢着把最后一批数据刷出去。这里给出一段采集时循环写数据的代码框架FIL file; UINT bw; uint8_t log_buf[128]; f_open(file, 0:/data.log, FA_OPEN_ALWAYS | FA_WRITE); f_lseek(file, f_size(file)); // 追加到文件末尾 while (1) { build_log(log_buf); f_write(file, log_buf, sizeof(log_buf), bw); if (write_count 10) { f_sync(file); write_count 0; } // 间隔1秒 } f_close(file);5. 文件系统损坏问题从chkdsk误判到掉电保护的设计取舍用着用着我最担心的事情还是发生了——一张SD卡从设备上拔下来插到电脑里Windows直接提示文件系统是RAWchkdsk告诉我无法处理。文件系统损坏了。这个问题在嵌入式和PC之间轮换使用的SD卡上特别常见通常有几种原因写入过程中突然掉电FAT表更新到一半目录项损坏。拔卡时机不对卡还处于写状态就直接被拔走。底层写命令后没有等待CTRL_SYNC卡还在忙数据根本没落盘。SPI模式下的信号质量差写入数据出现位反转。SPI模式SD卡信号质量问题是容易被忽视的元凶。SD卡的SPI模式速率上限一般是25MHz但因为线材、接触电阻、板子布局的原因实际跑到10MHz以上就可能出现偶发错误。我测试时把时钟从6.25MHz提到12.5MHz后跑千次读写循环没出错可放到长线缆场景就时不时冒一个FR_DISK_ERR。降低SPI时钟、让MOSI/MISO走短而粗的走线是低成本高收益的优化。再就是FATFS本身的健壮性配置。FF_FS_LOCK、FF_FS_MINIMIZE这些不是影响损坏的直接因素但能防止应用错误导致的缓冲区覆盖。真正提升文件系统健壮性的方式是加日志缓冲区数据先写入内存环形缓冲区定期刷盘降低写盘频次。文件循环覆盖用一个固定大小的日志文件写满后从头部重新开始避免无限增长导致FAT表过大。加上电自检挂载失败时先尝试f_mount失败再调用f_mkfs重新格式化保全设备可用性。我之前关注过一个很典型的现象设备端写文件一切正常卡拿到PC上只能看到以前的文件新增文件看不见。这是因为FAT表在Windows文件系统缓存里PC没有及时刷新。出现这种状况不代表嵌入式写入失败拿读卡器重新插拔或者换台电脑往往就正常了。所以做调试的时候不要一看到电脑上文件没更新就觉得是嵌入式代码的问题。6. 一个容易蒙圈的问题PC读不了卡而设备能读写问题出在哪到这里我忍不住想多说一个排查体验。设备端一切正常f_mount秒挂载f_write返回FR_OK数据能读回来但卡拿到PC上要么提示未格式化要么只有早期文件新数据完全看不见。这种只写不显的诡异情况我第一次遇到时排查了整整半天。先说结论多数情况是FAT表与目录项更新没落盘或者PC的文件系统缓存未刷新。解决办法是写完后调用f_mount(NULL, 0:, 1)做一次反挂载让FatFS把内部缓冲区全部刷进SD卡再等卡不在忙状态后断电。相当于嵌入式设备的安全弹出。如果你在设备端就能复现PC读不出来的情况那就用十六进制工具直接读SD卡的原始扇区看FAT1、FAT2和根目录区内容是否一致。FAT1和FAT2内容不一致基本上就说明写入过程中掉电了。FatFS默认是同步更新两份FAT的但如果底层disk_write乱序或者中途失败就可能只更新了一份。排查顺序是先确认CTRL_SYNC实现正确再确认写多块时地址连续最后再怀疑硬件信号。7. 速度实测与性能优化方向我最终在SPI 6.25MHz、FatFS单块读写、每扇区一个CMD17/CMD24的情况下做了速度测试操作实测速度连续写1MB文件约35KB/s连续读1MB文件约50KB/s打开已有文件追加写100条小日志每条128字节不sync约55KB/s说实话这个速度不算快但对我这种秒级记录频率的应用场景足够。如果你需要更快的速度方向有两个第一SPI时钟提到12.5MHz或25MHz理论上速度可以翻倍。但一定要做连续写压力测试尤其在温度变化后信号质量可能会有偏移。第二FatFS开启多扇区读写底层改用CMD18/CMD25多块命令。单块命令每次都要发送命令、等待响应、等待忙信号命令开销占比很高。多块命令只需要一次命令帧后面连续传输数据速度提升非常可观。第三个方向是用HC32F460的DMA配合SPI减少CPU干预SPI收发的时间。FatFS底层disk_read/disk_write本质上是大块数据搬运非常适合DMA。我初版代码是轮询SPI的一次读512字节要循环512次浪费了大量CPU时间。后来改成DMA加中断方式读速度直接翻倍。但注意DMA缓冲区必须按4字节对齐HC32F460的DMA对地址对齐有要求不满足会触发错误。HC32F460的DMA配置不算复杂先配好外设地址SPI数据寄存器、内存地址读写缓冲区、传输长度512 * count然后开传输完成中断即可。关键是要在传输前确保SPI的TX FIFO不卡住。之前一次偶发死锁就是因为我没处理SPI的TX空标志DMA数据还没发完就开始等RX中断了。8. 最后的调试建议与我的配置参考把这套东西调通之后我总结了几条最实用的经验SPI高速前先把低速初始化跑稳。很多问题卡在初始化阶段用示波器看MISO有没有波形比盲猜寄存器快得多。FatFS配置项一定要按自己的硬件来改尤其是FF_MAX_SS。这是最直接的内存杀手。第一次调通不要急着上DMA和多块。先用轮询单块把整链路跑通再加优化项否则出错都不知道是命令时序问题还是DMA配置问题。日志型应用务必设计掉电保护机制f_sync的频率要在数据安全性和写入寿命之间取平衡。Windows下遇到RAW文件系统先别急着格式化。把卡插回设备用f_mount尝试挂载后再做一次f_mkfs的准备工作能救则救。直接用电脑格式化等于放弃一切恢复机会。下面是我最终确认可用的ffconf.h关键配置项#define FF_USE_LFN 2 // 使用栈缓冲支持长文件名 #define FF_USE_MKFS 1 // 支持格式化 #define FF_FS_LOCK 2 // 最多同时打开2个文件 #define FF_VOLUMES 1 // 单卷 #define FF_MIN_SS 512 // 固定扇区大小 #define FF_MAX_SS 512 // 固定扇区大小 #define FF_FS_RPATH 1 // 启用相对路径 #define FF_USE_STRFUNC 1 // 启用f_printfSD卡加FatFS这套组合在MCU领域算是相当经典了网上的资料虽然多但真正能直接落地的细节往往藏在调试过程里。这篇文章把我踩过的坑都摆了出来尤其是内存占用、SPI时序、文件系统损坏这几个方面。希望能帮你少折腾几个晚上。我这个参考配置不敢说最优但至少是在HC32F460上实测可跑的。如果你在自己的板子上也发现了不一样的问题欢迎来交流大家一起填坑。本文还有配套的精品资源点击获取
返回列表