免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

STM32精英板接入MK SD NAND:SPI驱动与FatFS文件系统实战

STM32精英板接入MK SD NAND:SPI驱动与FatFS文件系统实战 把正点原子精英板的存储从“拆机容易焊”变成“一颗芯片搞定”这件事我前后折腾了两天。最开始手里项目的存储方案一直不太稳microSD卡接触不良、SPI Flash容量又不够后来拿到MK SD NAND这颗料研究了一下发现它本质就是把NAND Flash、控制器、坏块管理、ECC校验全封在了一颗SOP8封装的小芯片里外部走SPI或SDIO就能读写。这篇文章我用正点原子STM32精英板STM32F103ZET6来演示分成三个步骤把MK SD NAND完整跑通硬件接线、SPI底层驱动、FatFS文件系统挂载最后附上我排过的坑。如果你手上正好有精英板或者类似的F103开发板想给项目加一个大容量、可落地的文件存储这篇内容是直接可以照着抄作业的。1. 为什么我不再用microSD和SPI Flash先说结论MK SD NAND说白了就是一颗芯片形状的SD卡但比起传统方案它在嵌入式项目里的优势非常明显。1.1 MK SD NAND到底是什么MK SD NAND是MKD系列的一种自带管理控制器的NAND Flash芯片外部封装一般是SOP8也有LGA和BGA的变体。它内部集成了一颗原始NAND Flash颗粒一个SD控制器固件处理层包括坏块管理、ECC纠错、磨损均衡对外提供标准SD协议或SPI协议接口这意味着你在MCU侧完全不需要自己去管NAND的坏块、页映射、擦写均衡这些破事。对STM32来说它就像一个普通的SD卡发标准命令读扇区写扇区。但它又一不会被插拔、二不会接触不良、三体积小可以直接贴在PCB上特别适合量产的嵌入式文件存储场景。正点原子精英板板上自带了一个microSD卡座但卡座的可靠性在实际项目里是个隐患震动、氧化、插拔次数多了都会出问题。而W25Q128这种SPI Flash也就是128Mbit拿来做日志和配置还行做图片、录音、离线数据缓存就不够看了。1.2 三步骤方案的核心链路我在精英板上跑通MK SD NAND整体链路是这样的硬件层精英板SPI1作为主机接口接到MK SD NAND的SPI引脚供电走3.3V。协议层在STM32上跑SD Card SPI模式协议实现初始化、读扇区、写扇区。文件层挂载FatFS文件系统让上层直接用f_open、f_write、f_read操作文件。这个结构和驱动普通SD卡几乎一模一样区别只是物理接口从卡座换成焊盘。所有经验可以直接迁移到其他板卡上这也是我选择这个方案的原因之一。1.3 使用场景和适用人群只要你需要设备断电后还能保存数据且数据量在几十MB到几个GB之间MK SD NAND都合适。典型场景包括离线数据记录仪音频/语音播放器嵌入式GUI的图片字库存储物联网网关的本地缓存医疗或工业设备运行日志这套教程适合有一定STM32基础、会使用CubeMX或标准库、对文件系统有概念但没真正在单片机上跑过SD NAND的读者。如果你刚开始学STM32建议先把GPIO、SPI、串口这些基础外设过一遍再来折腾。2. 第一步硬件接线与板上资源评估2.1 精英板的SPI1引脚排查正点原子精英板主控是STM32F103ZET6板载资源非常多但我不打算用板子自带的SD卡座而是把SPI1引出来接MK SD NAND。SPI1在F103ZET6上对应的引脚是信号STM32引脚精英板丝印MK SD NAND引脚SCKPA5SPI1_SCKSCLKMISOPA6SPI1_MISODOMOSIPA7SPI1_MOSIDICSPA4普通IOCS精英板上PA5、PA6、PA7是引出了排针的PA4也可以通过杜邦线或者飞线连出来。我这里图省事直接用杜邦线接了一个SOP8转接模块如果你打算量产或者做长期稳定测试建议直接把芯片贴到PCB上。2.2 供电和上拉电阻处理MK SD NAND的工作电压一般是2.7V到3.6V精英板板载的3.3V稳压输出正好满足要求。需要注意以下几点电源端并一个0.1uF陶瓷电容靠近VCC引脚放。MD SD NAND的DO引脚是推挽输出不需要外部上拉但如果走线很长建议加4.7k到10k上拉到3.3V。CS引脚绝对不要悬空初始化或者复位阶段保持高电平否则芯片收到乱序命令容易锁死。STM32的PA4做普通GPIO输出带开漏或者推挽都可以但推挽输出最省事。2.3 接线后的预检查实际通电之前我说的预检查非常简单万用表量一下VCC对GND是否短路再量一下SCK、DI、DO、CS对地有没有异常短接。别觉得这一步多余我之前焊过一块转接板焊盘之间桥连了都不知道上电后芯片发热差点烧掉。焊接SOP8这种引脚松香别涂太多烙铁温度280到320摄氏度焊完用放大镜检查引脚之间有没有锡珠。3. 第二步SPI驱动与SD初始化3.1 CubeMX配置SPI1在精英板上用CubeMX配置工程我只开SPI1和USART1方便调试。关键配置如下SPI1模式Full-Duplex Master数据帧格式8 Bits时钟极性CPOLLow时钟相位CPHA1 EdgeNSSSoftware波特率预分频在初始化阶段建议先设置128分频即系统时钟72MHz除以128得到约562.5kHz为什么初始化阶段偏要用慢速因为SD协议规定上电初始化时SPI时钟必须在100kHz到400kHz范围内最稳妥的做法是先慢后快。很多人在STM32上跑SD卡失败就是初始化阶段直接用18MHz结果卡在CMD8或者ACMD41上。CubeMX生成初始化的代码之后我手动建立一个底层文件sd_spi.c核心是这两个函数void SD_SPI_Slow(void) { hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_128; HAL_SPI_Init(hspi1); } void SD_SPI_Fast(void) { hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; HAL_SPI_Init(hspi1); }3.2 SD Card SPI协议移植SD卡在SPI模式下采用的是命令应答机制主机先发6字节命令帧格式是命令索引、4字节参数、1字节CRC。CRC在SPI模式下除了CMD0和CMD8其余命令可以忽略直接发0xFF占位。但为了稳妥我移植的时候还是把固定命令的CRC值算好了。底层最核心的地方是数据收发函数uint8_t SD_SendByte(uint8_t data) { uint8_t rx; HAL_SPI_TransmitReceive(hspi1, data, rx, 1, 100); return rx; } uint8_t SD_ReadByte(void) { return SD_SendByte(0xFF); } void SD_CS_Enable(void) { HAL_GPIO_WritePin(SD_CS_GPIO_Port, SD_CS_Pin, GPIO_PIN_RESET); } void SD_CS_Disable(void) { HAL_GPIO_WritePin(SD_CS_GPIO_Port, SD_CS_Pin, GPIO_PIN_SET); SD_SendByte(0xFF); }每次CS拉高之后必须多发一个0xFF字节作为SD卡的额外时钟这是协议层面的要求目的是让SD卡在片选释放之后还能完成内部操作。3.3 初始化的完整流程SD卡在SPI模式下的初始化顺序是有讲究的我按生产批次、固件版本反复踩过几回之后固定成下面的流程第一步上电延时和时钟预热。拉低CS之前先发至少74个以上时钟周期我习惯循环调用10次SD_ReadByte()也就是80个时钟周期确保SD NAND内部上电稳定。第二步发送CMD0。命令字节为0x40参数全0CRC固定为0x95。如果SD NAND正确响应R10x01说明它进入了SPI模式。第三步发送CMD8。参数为0x000001AACRC固定为0x87。CMD8是SD V2.0协议识别防火墙用来区分旧卡和新卡。响应R10x01之后还要读取4字节的返回数据其中低8位是0xAA用来校验接口电压是否匹配。第四步发送CMD55ACMD41组合。ACMD41用于启动卡内部的初始化过程。这里需要循环每发送一次CMD55和ACMD41之后判断响应如果返回0x00说明卡初始化完成如果一直返回0x01就继续循环。我设置最大循环次数为2000次超过之后报超时。第五步发送CMD58读取OCR寄存器判断卡是否支持高容量。如果读取OCR返回的bit30为1说明是SDHC卡MK SD NAND大容量版本一般都会置位这一位。这个信息在后续FatFS挂载时并不必然需要手动处理因为FatFS会自动判断但做底层驱动移植时做检查可以有更明确的状态机。初始化代码里还要注意一个小坑ACMD41之前必须先发CMD55如果CMD55返回0x01而ACMD41返回0x01属于正常循环。但如果CMD55返回的值是0x05等非法状态那说明SPI模式没有真正进入八成是上电时钟脉冲没给够或者CS时序不对。3.4 读扇区与写扇区MK SD NAND在SPI模式下的单次读写扇区大小固定为512字节。读扇区利用CMD17写扇区利用CMD24读扇区命令成功后数据块以起始令牌0xFE开头后面跟512字节数据加2字节CRC。CRC在读取时可以不检查直接丢弃。写扇区命令成功与否取决于最后一个字节的写状态标志具体判断规则要区分SD卡版本和芯片固件这里提供一个通用模板uint8_t SD_WriteSector(uint32_t sector, uint8_t *buf) { uint16_t i; uint8_t resp; uint32_t timeout; resp SD_SendCmd(CMD24, sector 9, 0x01); if (resp ! 0x00) return 1; SD_SendByte(0xFE); for (i 0; i 512; i) { SD_SendByte(buf[i]); } SD_SendByte(0xFF); SD_SendByte(0xFF); timeout 0xFFFFFF; while (timeout--) { resp SD_ReadByte(); if ((resp 0x1F) 0x05) break; } if ((resp 0x1F) ! 0x05) return 2; timeout 0xFFFFFF; while (timeout--) { if (SD_ReadByte() 0xFF) break; } return 0; }扇区地址是字节地址的高21位所以要把LBA地址左移9位。这里有一个很容易踩的坑如果你读出来的容量是514MB这种非整数GB值不要惊讶MK SD NAND的标称容量采用十进制和文件系统二进制的容量显示本身就有差异这是正常现象。我在整个底层驱动实现完之后会先做一段裸测试连续读一个扇区连续写一个扇区再读回来比对。不走文件系统纯看数据是否正确。这一步通过率高后面挂FatFS就会顺很多。4. 第三步接入FatFS文件系统4.1 移植FatFS到工程接好底层驱动文件系统阶段相对轻松。我这里用的是正点原子例程里自带的FatFS文件系统代码也可以从ELM Chan官网下载最新的ff15版两者结构大同小异。需要加入源码文件包括ff.c ff.h ffconf.h diskio.c diskio.h文件系统要对底层驱动暴露5个核心函数每个函数都是底层SD驱动和上层文件逻辑之间的桥梁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)其中disk_read和disk_write里直接调用我前面写的SD_ReadSector和SD_WriteSector。diskio.c里需要特别实现disk_ioctl的CTRL_SYNC、GET_SECTOR_COUNT、GET_SECTOR_SIZE、GET_BLOCK_SIZE这几个查询分支否则FatFS挂载时会报格式化错误。4.2 FatFS配置要点ffconf.h里几个关键宏我的配置思路如下配置项值说明FF_USE_LFN1或2开启长文件名否则中文文件或长文件名会打开失败FF_FS_MINIMIZE0保留全部API调试阶段别精简FF_USE_STRFUNC1允许f_printf方便写log测试FF_VOLUMES1只挂一个物理盘FF_MULTI_PARTITION0单分区足矣FF_MAX_SS4096兼容扇区大小大于512的设备开启长文件名之后FatFS会增加一个WORKING_BUFFER临时缓冲需要在ffconf.h确认FF_LFN_BUF定义一般设255或是改成动态分配方式。如果不开LFN只是用简单的8.3短文件名也能跑但实际项目中根本不够用。4.3 挂载、建文件、读写实测挂载文件系统代码非常简单FATFS fs; FIL file; FRESULT res; res f_mount(fs, 0:, 1); if (res FR_OK) { printf(mount ok\r\n); }第一次使用一块全新的MK SD NAND时芯片出厂可能已经带FAT文件系统也可能没有需要格式化。这里有一个经验如果f_mount返回FR_NO_FILESYSTEM说明芯片上没有有效文件系统调用f_mkfs做一次格式化即可res f_mkfs(0:, NULL, 0, 0);mkfs操作会整盘格式化时长取决于容量和SPI速度。我的256MB版本在18MHz SPI下大约需要十几秒。做格式化之前务必确认芯片里没有重要数据这一步不可逆。格式化和挂载都正常后接下来就是标准的文件操作流程res f_open(file, 0:/test.txt, FA_CREATE_ALWAYS | FA_WRITE); if (res FR_OK) { f_write(file, Hello MK SD NAND\r\n, 18, bw); f_sync(file); f_close(file); }读文件也差不太多f_open之后f_read然后f_close。我实测时的串口输出大概是这样的SD init ok card capacity: 256 MB mount ok file write ok, size 18 file read: Hello MK SD NAND这个流程跑通之后你可以用f_open和f_write把任何结构化的数组、日志、传感器数据、图标字库写进去。就算中途断电只要在关键节点调用了f_sync之前写入的数据也不会丢。4.4 长文件名和中文编码乱码问题项目里如果用中文字符串做文件名FatFS的API默认只接受Unicode或GBK编码取决于FF_LFN_UNICODE配置。我个人的建议是嵌入式环境里尽量用英文加数字做文件名中文文件名在低资源MCU上容易把编码问题放大。真正需要中文的时候在PC端建好文件再拷进SD NAND比MCU端写中文靠谱得多。5. 常见问题排查与避坑经验整个过程跑下来真正有价值的部分是排坑阶段。我把这三天遇到的典型问题整理成一张速查表后续有人遇到类似问题可以照单抓药。现象可能原因排查方法上电后芯片发热VCC和GND接反或者引脚桥连断电后测电阻重新焊接RCC发CMD0无响应SPI时钟太快把预分频调到128重新初始化CMD8返回0x00卡没有支持SD V2.0协议确认芯片型号和固件版本尝试跳过CMD8ACMD41一直超时CS时序不对或上电时钟脉冲不够拉高CS后补20个0xFF拉低CS后慢时钟重试FatFS挂载返回FR_NO_FILESYSTEM芯片无文件系统用f_mkfs格式化一次写入后读回来不对扇区地址或写入令牌错误每次都检查返回的写状态掩码文件创建成功但写入失败没有调用f_sync或f_close写入后立即f_sync大文件读取乱码FatFS的LFN未开启设置FF_USE_LFN为1SPI数据线干扰杜邦线太长尽量缩短线缆加屏蔽等待稳定电平5.1 逻辑分析仪定位底层时序我在移植过程中最开始ACMD41一直过不去后来用逻辑分析仪抓了SPI线上波形发现CS释放之后没有补时钟周期下一帧命令来了之后SD NAND内部还处于忙状态导致状态机错乱。这是测试调试的关键点。具体方法是把LA_CH0挂SCLKLA_CH1挂CSLA_CH2挂MOSILA_CH3挂MISO。采集命令阶段把采样率设为10MHz以上因为SPI慢速阶段时钟只有500kHz低速采样就能看清。SD命令帧6字节分别是第一个字节高6位是命令号参数占4个字节最后一个字节在CMD0和CMD8时是高有效CRC。通过逻辑分析仪检验主机发出来的数据是否正确能确认是MCU侧问题还是SD NAND侧问题。5.2 硬件走线的一个教训用杜邦线连接MK SD NAND转接板和精英板的时候刚开始测试一切正常后来我把SPI时钟调到18MHz就会偶尔出现读扇区数据出错。逻辑分析仪上看MISO上的上升沿毛刺很多最后解决方法是把杜邦线缩短到10cm以内并且MISO线上对地并联一个100pF的小电容滤波。如果你直接把MK SD NAND焊在PCB上这块问题基本不会遇到。5.3 注意芯片型号后缀的含义MK SD NAND不是一个单一型号而是系列命名。不同的后缀对应不同的容量和SPI时钟能力购买时务必确认清楚比如MK-01G、MK-02G、MK-04G这种后缀一般代表容量部分型号标了支持SPI模式部分只标SD模式芯片表面的固件版本号也可能影响命令兼容性如果你用的是小容量版本比如128MB初始化流程本身完全一致。但大容量版本在格式化时耗时更长写满之后出现坏块的触发概率会更高好在内部的磨损均衡和坏块管理会把你从这些底层问题中解放出来你只需要关心扇区读写返回状态。5.4 关于掉电保护文件系统写入过程中如果突然掉电是有可能损坏FAT表或者目录项的。虽然MK SD NAND内部固件会尽量保证单个扇区写入原子性但多扇区级联的写操作仍然受文件系统本身的一致性制约。我的建议是高频写日志时每写一条就f_sync一次关键配置数据可以写两分区双备份如果对可靠性要求极高选工业级温度范围的SD NAND这些措施比单纯堆硬件来得更实际。6. 后续还能怎么扩展这一套跑通之后等于给精英板加了一个通用的文件存储底座。后续可以做不少延伸把FatFS换成LittleFS面向更注重掉电保护日志场景把SPI通信换成SDIO接口将读写速度提升到几十MB级别用SD NAND做字库和图片存储配合LVGL、STemWin这类GUI库直接把界面资源放外部文件系统加上OTA功能把升级固件放到SD NAND通过Bootloader读取并跳转执行我这次用MK SD NAND的另一个原因是它在做批量生产时比microSD卡位成本低、一致性高出厂贴片就能用不用每台设备再插卡测试。如果你也在做一个需要存储的小批量设备我建议直接学完这套流程后拿一块空PCB做打样验证提前把封装和供电规划好后面就真的是一颗芯片解决所有问题。我在实际调试中最深的体会是SD NAND驱动这件事逻辑不难难在时序和细节。只要把上电时钟、CMD0、CMD8那几步吃透后面都是水到渠成。最后再分享一个小技巧如果你手头有逻辑分析仪但不确定自己的命令时序对不对可以先模拟发送一遍命令把6字节的十六进制码抓出来和SD协议文档一个个比对这比盲改代码快得多。
返回列表