ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32 SPI读写SD卡完整指南:从底层命令到FatFs文件系统

STM32 SPI读写SD卡完整指南:从底层命令到FatFs文件系统 做嵌入式的朋友应该都有过这种经历手里一块STM32F103C8T6最小系统板想存点配置参数、记录运行日志或者放几张图片进去板载Flash明显不够用目光自然就落到了SD卡上。SD卡有SD模式和SPI模式两种玩法对F103这种不带SDIO外设的芯片来说SPI模式几乎是最省引脚、最稳的读写方案。这次我就用STM32F103C8T6配合SPI1把SD卡读写从底层命令到文件系统完整跑通全程压缩成5个关键步骤附上可以直接抄的完整代码希望能帮有同样需求的朋友少走几条弯路。1. 为什么是SPI模式SD卡其实有两副面孔1.1 SD卡协议的两个面孔SD模式与SPI模式很多人第一次接触SD卡时看到卡背面那么多金手指就头大。其实SD卡内部就是一颗NAND Flash加一颗主控芯片对外暴露的接口是一套命令/响应机制类比一下它就像一套“远程调用协议”主机发一条命令卡回一个状态然后按需传数据。这套协议在物理层有两种形态。第一种是SD模式命令线是独立的CMD引脚数据走DAT0~DAT3四根线速度快、时序复杂而且必须有SDIO控制器硬件配合。第二种是SPI模式SD卡被模拟成一个标准SPI从设备主机通过SCK、MOSI、MISO、CS四根线发命令字节、收响应字节数据块也是按字节流走逻辑上跟操作一颗SPI NOR Flash差不多。对STM32F103C8T6来说选SPI模式不是一道选择题而是一道判断题这颗芯片根本没有SDIO外设想跑SD模式就只能靠GPIO硬模拟费劲且不稳定。SPI模式只需要芯片自带的SPI1外设配置简单、代码量小还方便后续接其它SPI设备共用总线。1.2 选SPI模式的四个理由和一个代价先说理由。第一是引脚极省SD模式最少也要1条CMD加4条数据线加1条CLKSPI模式只需要SCK、MOSI、MISO、CS四条线留给其它外设的引脚多得多。第二是排查方便每一笔操作都是“发命令字节→收响应字节→传数据”卡在哪个环节一眼就能看出来配合逻辑分析仪甚至能逐字节对齐波形。第三是兼容性好市面上几乎任何SD卡、TF卡加卡套都能进入SPI模式包括一些杂牌卡和低速卡。第四是速度对单片机场景够用实测SPI模式下的读速度能到1~2MB/s写速度500KB/s左右记录日志、存配置文件绰绰有余。代价就是别指望它跑出SD模式几十MB/s的速度。不过F103本身主频72MHz也没有SDIO控制器就算勉强模拟高速模式CPU也被拖死。真追求速度正确路线是换带SDIO的型号比如F407/F429或者在F103上把SPI配置到18MHz再加DMA传输后面我会讲DMA配合思路。补充一句容易踩坑的话SD卡SPI模式下的最大时钟频率规范是25MHzF103的SPI1最高18MHz所以即使把分频系数拉到4也还在卡的能力范围内可以放心用。2. 硬件准备与接线一个细节能让你排查一整夜2.1 物料清单与最小系统需求这次实验的物料非常简单可以说手头有这些就能开工STM32F103C8T6最小系统板一块淘宝上蓝色那种即可TF卡一张加一个TF转SD卡套或者直接用SD卡支持SPI模式的SD卡模块或者自己搭卡座电路杜邦线若干、面包板可选逻辑分析仪强烈建议准备一台排查时序问题事半功倍最小系统板自带LDO稳压和USB转串口不用额外搭电源电路。SD卡模块有两种常见形态一种是带电平转换的多合一模块适合和5V单片机配合另一种是直插卡座引脚直接引出来适合3.3V系统。F103是3.3V供电用直插卡座接最干净模块上的电平转换芯片反而可能引入额外延迟。2.2 SPI1引脚映射与接线对照我这套代码使用SPI1引脚分配如下信号引脚方向说明SCKPA5MCU → SDSPI时钟MISOPA6SD → MCU数据输入注意方向MOSIPA7MCU → SD数据输出CSPA4MCU → SD片选软件控制接线时最容易犯的错就是把MOSI和MISO接反因为两块板子的丝印标识方式不一样有的标SDI/SDO有的标MOSI/MISO。判断标准是MOSI是主设备输出从设备输入在SD卡这边叫DIData InMISO是主设备输入从设备输出在SD卡这边叫DOData Out。接反的典型症状是发什么命令都读不到有效响应读回来的永远是0xFF。建议接线后先拿逻辑分析仪抓一下确认SCK上有时钟、MOSI上有命令帧再往下做。2.3 供电、上拉与开关顺序的讲究SD卡SPI模式的工作电压是3.3V直接接5V会烧卡这点每个教程都会说但我还是想再强调最小系统板上的3.3V输出电流一般只有几十到几百毫安本身足够驱动卡但如果板子供电来自USB延长线线材质量差会导致压降卡工作在临界电压下典型表现就是偶尔初始化失败、读写时好时坏。上拉电阻方面SD卡规范建议CS、SCK、MOSI都接10k左右的上拉电阻这主要是防止总线悬空时产生误动作。多数卡座模块上已经集成但如果用直插卡座自己接线最好在初始化的CS线和MISO线上各加一个10k上拉实在没有也可以先不接因为F103的GPIO可配置内部上拉很多工程不接外部电阻也跑得好好的。上电顺序也有讲究先给微控制器上电再插卡。如果反过来卡先上电、MCU还没初始化SPI引脚总线电平不确定可能导致SD卡进入异常状态。更保险的做法是程序启动后自动执行一次完整的SD卡初始化流程这样即使卡先被插上也能恢复正常。我习惯在初始化SPI和GPIO后先把CS拉高至少500ms再开始发时钟脉冲给卡足够的时间稳定。3. 5步核心流程从上电到读写扇区3.1 第一步SPI外设和GPIO初始化先把时钟降到400kHz以下很多人SD卡初始化失败根本原因不是代码逻辑而是初始化时SPI时钟太快。SD卡规范明确要求卡在识别初始化阶段主机时钟频率必须控制在100kHz~400kHz之间成功进入SPI模式后才能提速。这是因为卡刚上电时内部逻辑还没准备好高频时钟下响应会乱套。我用CubeMX配置的时候SPI1参数是这样选的全双工主模式、8位数据、SPI时钟极性LOW、相位1EDGE也就是CPOL0、CPHA0对应SPI Mode 0。SD卡规范允许Mode 0和Mode 3但Mode 0最常用几乎所有卡都支持。初始化阶段的分频系数我直接拉到256分频也就是72MHz除以256等于281.25kHz落在规范要求范围内。等SD卡完成识别后再重新初始化SPI把分频系数改成4时钟提到18MHz。分段设置时钟是这套流程能稳定跑起来的关键之一。对应CUBEMX生成的初始化代码核心部分如下我用标准HAL库void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_SPI1_CLK_ENABLE(); // PA5 SCK, PA7 MOSI 复用推挽输出 GPIO_InitStruct.Pin GPIO_PIN_5 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PA6 MISO 浮空输入 GPIO_InitStruct.Pin GPIO_PIN_6; GPIO_InitStruct.Mode GPIO_MODE_INPUT; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // PA4 CS 推挽输出默认拉高 GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // SPI1 配置 hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_256; // 281.25kHz hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }3.2 第二步先发74个时钟脉冲再用CMD0让卡进入SPI模式SD卡上电后默认工作在SD模式要切到SPI模式必须执行一个特殊的握手流程这个流程是很多新手卡壳的地方。具体动作是先把CS拉高连续向SD卡发送至少74个时钟脉冲。因为SPI总线上没有数据可发时空闲电平是1所以发0xFF就等效于产生时钟脉冲。为什么是74SD卡规范说至少74我习惯发80个时钟也就是循环10次0xFF留点余量。这个过程是让SD卡内部逻辑在纯时钟信号下稳定下来识别出总线状态。然后拉低CS发CMD0命令格式是6个字节其中第1字节是命令索引0x40第2到第5字节是参数第6字节是CRC。这里有个极其重要的细节CMD0的CRC必须填0x95不能随意写。原因是卡此时还在SD模式SD模式要求命令带合法CRC只有进入SPI模式后CRC校验才被关闭。如果CMD0的CRC写错卡永远不会产生有效的R1响应。发完CMD0后卡会返回一个R1响应字节如果是0x01表示卡进入“空闲状态”SPI模式切换成功。如果响应超时或者返回0xFF流程就要重来。接着要处理卡类型。我发CMD8参数为0x000001AACRC填0x87用来区分SD V1.x和SD V2.x。CMD8返回0x01说明是支持SD V2.x的卡然后读取后面4字节返回内容可以判断卡支持2.7~3.6V供电最后再循环发CMD55ACMD41直到就绪CMD8返回0xFF或者无响应说明是SD V1.x的卡直接走CMD55ACMD41流程。ACMD41是应用特定命令前面必须先发CMD55告诉卡“下一条是应用命令”。ACMD41的参数如果是0x40000000表示请求高容量SDHC卡如果是0x00000000表示普通SDSC卡。初始化代码要循环发送CMD55和ACMD41直到R1返回0x00表示卡退出空闲状态准备就绪。这段初始化代码我写得比较保守每一个超时判断都有明确的错误码返回方便排查uint8_t SD_SPI_Transfer(uint8_t data) { uint8_t rx 0xFF; HAL_SPI_TransmitReceive(hspi1, data, rx, 1, 100); return rx; } uint8_t SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc) { uint8_t frame[6]; frame[0] cmd | 0x40; frame[1] (arg 24) 0xFF; frame[2] (arg 16) 0xFF; frame[3] (arg 8) 0xFF; frame[4] arg 0xFF; frame[5] crc; for (int i 0; i 6; i) SD_SPI_Transfer(frame[i]); // 最多等待8个字节直到读到有效的R1响应 for (int i 0; i 8; i) { uint8_t resp SD_SPI_Transfer(0xFF); if ((resp 0x80) 0) return resp; } return 0xFF; } uint8_t SD_Init(void) { uint8_t resp; uint32_t timeout; CS_HIGH(); for (int i 0; i 10; i) SD_SPI_Transfer(0xFF); // 80个时钟脉冲 CS_LOW(); // 进入SPI模式 resp SD_SendCmd(0, 0x00000000, 0x95); if (resp ! 0x01) return 1; // CMD0失败 // 区分卡类型 resp SD_SendCmd(8, 0x000001AA, 0x87); if (resp 0x01) { // SD V2.x读取4字节返回 for (int i 0; i 4; i) SD_SPI_Transfer(0xFF); // 循环ACMD41请求高容量 timeout 10000; do { SD_SendCmd(55, 0x00000000, 0x01); resp SD_SendCmd(41, 0x40000000, 0x01); } while (resp ! 0x00 --timeout); if (resp ! 0x00) return 2; // ACMD41失败 // CMD58读取OCR判断是否是SDHC resp SD_SendCmd(58, 0x00000000, 0x01); for (int i 0; i 4; i) resp SD_SPI_Transfer(0xFF); } else { // SD V1.x循环ACMD41 timeout 10000; do { SD_SendCmd(55, 0x00000000, 0x01); resp SD_SendCmd(41, 0x00000000, 0x01); } while (resp ! 0x00 --timeout); if (resp ! 0x00) return 3; // V1卡ACMD41失败 } // 初始化完成提速到18MHz hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; HAL_SPI_Init(hspi1); return 0; }注意ACMD41循环里两条命令之间的时间间隔别太紧有条件的话每次循环之间发几个0xFF时钟给卡一点处理时间。有些卡对连续命令响应特别慢连续猛发会导致偶发失败。3.3 第三步读一个扇区从CMD17到数据令牌初始化成功后SD卡就是一颗“有擦写要求的SPI Flash”最小读写单位是扇区一个扇区512字节。读某个扇区用CMD17块地址参数是字节地址也就是扇区号乘以512。一次完整读扇区流程是这样的拉低CS发CMD17参数是目标字节地址第6字节CRC随便填因为SPI模式下CRC校验已经关闭但这一字节不能不填命令帧固定6字节。然后等待R1响应正常是0x00说明卡接受命令。接着卡开始读内部Flash准备好数据后会在MISO线上发出一个0xFE起始令牌。这中间有一段数据准备时间需要不断发送时钟0xFF去读状态读到0xFE说明后面紧跟512字节有效数据。读的时候有个细节0xFE之前可能出现其它值一般是0xFF表示还在准备中。如果读到0x0F这是数据错误令牌说明卡内部读出了坏块或者地址越界。我习惯把0x0F单独判断出来返回错误码方便定位。读完512字节后后面还有2字节CRCSPI模式下可以忽略但必须把它们读出来否则卡的状态机不会进入下一命令。很多人漏掉这一步导致读第一个扇区正常、读第二个扇区时响应错乱。uint8_t SD_ReadSector(uint32_t sector, uint8_t *buf) { uint8_t resp; uint32_t timeout; CS_LOW(); resp SD_SendCmd(17, sector 9, 0x01); if (resp ! 0x00) { CS_HIGH(); return 1; } // 等待数据起始令牌0xFE timeout 100000; while (timeout--) { resp SD_SPI_Transfer(0xFF); if (resp 0xFE) break; if (resp 0x0F) // 数据错误 { CS_HIGH(); return 2; } } if (timeout 0) { CS_HIGH(); return 3; // 等待令牌超时 } // 读取512字节数据 for (int i 0; i 512; i) buf[i] SD_SPI_Transfer(0xFF); // 跳过2字节CRC SD_SPI_Transfer(0xFF); SD_SPI_Transfer(0xFF); CS_HIGH(); return 0; }3.4 第四步写一个扇区关键在等待“不忙”写扇区比读扇区多一个“忙状态检测”这是最容易出问题的地方。写操作用CMD24同样把扇区号乘512作为字节地址。命令发出去后R1返回0x00代表卡接受写命令然后不能马上发数据要先发一个数据起始令牌0xFE紧接着发512字节数据最后跟2字节CRCCRC内容忽略写0xFF即可。发完数据后卡会返回一个“数据响应”字节代表这512字节写入的结果。这个字节的低5位如果等于0x05说明卡接受了数据。要特别说明0x05表示数据接受0x0B表示数据写错误0x0D表示数据写错误且忙这几个值要区分开。关键来了写完一个扇区后卡需要把缓冲区的数据实际写入Flash这个过程可能长达几十毫秒甚至上百毫秒期间卡会拉低MISO表示“忙”。主机必须不停地发时钟0xFF持续检测MISO线直到读到0xFF说明卡不忙了才能发出下一条命令。如果没等忙状态结束就发下一命令轻则命令被吞掉重则数据错乱。uint8_t SD_WriteSector(uint32_t sector, const uint8_t *buf) { uint8_t resp; uint32_t timeout; CS_LOW(); resp SD_SendCmd(24, sector 9, 0x01); if (resp ! 0x00) { CS_HIGH(); return 1; } // 数据起始令牌 SD_SPI_Transfer(0xFE); // 512字节数据 for (int i 0; i 512; i) SD_SPI_Transfer(buf[i]); // 2字节CRC忽略 SD_SPI_Transfer(0xFF); SD_SPI_Transfer(0xFF); // 读数据响应低5位为0x05表示接受 resp SD_SPI_Transfer(0xFF); if ((resp 0x1F) ! 0x05) { CS_HIGH(); return 2; } // 等待卡不忙 timeout 100000; while (SD_SPI_Transfer(0xFF) ! 0xFF timeout--); if (timeout 0) { CS_HIGH(); return 3; // 等待忙超时 } CS_HIGH(); return 0; }3.5 第五步用FatFs把扇区变成文件而不是整天操作字节到这一步底层扇区读写已经能工作了。但如果你在业务代码里直接操作扇区比如自己设计文件分配表那基本是在造轮子。成熟方案是移植FatFs文件系统组件把disk_read和disk_write这两个底层接口对接上去上层就能用f_open、f_write、f_read直接操作文件名。FatFs是面向嵌入式场景的FAT文件系统库代码量小、移植简单。它会替你维护文件分配表、目录项、坏块标记这些事情。你需要做的就是在diskio.c里实现几个接口函数其中最核心的是disk_initialize对应SD卡初始化disk_read对应扇区读disk_write对应扇区写disk_ioctl返回容量、扇区大小等信息。DRESULT disk_initialize(BYTE pdrv) { if (pdrv ! 0) return RES_PARERR; if (SD_Init() 0) return RES_OK; return RES_ERROR; } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { for (UINT i 0; i count; i) { if (SD_ReadSector(sector i, buff i * 512) ! 0) return RES_ERROR; } return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { for (UINT i 0; i count; i) { if (SD_WriteSector(sector i, buff i * 512) ! 0) return RES_ERROR; } return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { if (pdrv ! 0) return RES_PARERR; switch (cmd) { case GET_SECTOR_COUNT: // 从CSD寄存器解析容量实测后填入 *(DWORD *)buff 15466496; // 以卡片实际容量为准 return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff 1; return RES_OK; default: return RES_PARERR; } }4. 完整代码骨架与关键配置说明4.1 工程结构怎么组织我习惯把SD卡相关代码拆成两个文件一个是sdcard.c负责SPI字节收发、命令发送、SD初始化、扇区读写另一个是diskio.c在FatFs原有框架上实现接口。这样底层和文件系统层分离哪一层出问题就在哪一层查。main函数里则是典型的文件操作流程#include ff.h FATFS fs; FIL file; UINT bw, br; char write_buf[] Hello SD Card!\r\n; char read_buf[64] {0}; int main(void) { HAL_Init(); SystemClock_Config(); SPI1_Init(); // 挂载文件系统格式化参数为1表示立即挂载 if (f_mount(fs, , 1) ! FR_OK) { // 挂载失败可能卡未格式化可尝试 f_mkfs } // 创建并写入文件 if (f_open(file, hello.txt, FA_CREATE_ALWAYS | FA_WRITE) FR_OK) { f_write(file, write_buf, strlen(write_buf), bw); f_close(file); } // 打开并读取 if (f_open(file, hello.txt, FA_READ) FR_OK) { f_read(file, read_buf, sizeof(read_buf), br); f_close(file); } while (1) { } }4.2 关于SPI速率、DMA和中断的取舍底层初始化和文件系统打通之后大多数人会关心速度。这里给一组我实测过的参考数据SPI时钟18MHz时连续读扇区大约1.5MB/s连续写扇区大约500KB/s。之所以读快写慢是因为FAT文件系统写一个文件时往往要先写目录项、再写数据、再回写FAT表每个扇区之间还夹杂着卡内部Flash擦写时间。如果觉得CPU被占用太久可以引入DMA。STM32F103C8T6的SPI1支持DMA发送和接收一次DMA搬512字节CPU只在传输完成中断里处理一帧。不过要注意SD卡SPI模式下每读一个字节必须同时发一个时钟DMA全双工模式下发送缓冲要填0xFF接收缓冲存有效数据。代码大致思路是固定一个512字节的发送缓冲区全部填0xFF再设置接收缓冲区然后调用HAL_SPI_TransmitReceive_DMA。如果做文件系统FatFs每读一个扇区就触发一次DMACPU利用率能明显降下来。4.3 关于国产替代芯片的兼容性STM32F103C8T6是意法半导体的经典型号这几年国产同型号引脚兼容芯片用得很多。我试过国产同名芯片直接编译同一套代码SPI外设寄存器级完全兼容HAL库也能直接用SD卡读写没有任何区别。采购时要确认芯片丝印和批次有的兼容芯片在低功耗模式或时钟树细节上略有差异但只要按数据手册把外部晶振和启动引脚配置对这套SD卡代码可以无缝跑起来。5. 常见问题与排查技巧实录5.1 初始化卡在CMD0响应永远是0xFF这是SD卡SPI入门最常见的坑几乎可以列出十个原因。按概率排序第一是接线错误MOSI和MISO接反第二是CS引脚没拉低CMD0必须在CS为低电平期间发送第三是上电后没发足够多的时钟脉冲74个时钟是硬性条件第四是初始化阶段SPI时钟太高建议先降到256分频第五是供电不足换一根粗短的USB线试试。排查手段有一个高效的逻辑分析仪抓SPI波形。正常CMD0发送后MISO上应该出现至少一个非0xFF的字节。如果命令字节已经发出但MISO一直高电平基本可以断定卡没进入SPI模式问题就在前置时序上。5.2 ACMD41一直得不到0x00响应ACMD41失败有两个常见原因。一是CMD8分支没处理好把不支持的卡强行按SD V2.x流程跑导致ACMD41参数不同卡始终处于忙状态。二是ACMD41循环里两条命令之间间隔太短卡还在处理上一条命令。如果程序里没有特意插入延时建议在CMD55和ACMD41之间发几个0xFF时钟给卡一点喘息时间。还有一个容易被忽略的点ACMD41的参数在高容量位HCS上。如果卡是SDHC而ACMD41参数没有置位HCS卡可能永远不返回0x00反过来也一样。所以SD V2.x分支用0x40000000SD V1.x分支用0x00000000不要混用。5.3 能读扇区但读到的一堆0xFF读出来的数据全是0xFF说明MISO线上没有有效数据。这时候先确认是不是卡没插好卡套的金属弹片接触不良经常导致MISO虚接。再用万用表量一下MISO引脚的电平正常情况下空闲时应该被上拉为高读写时有脉冲变化。也有一种情况是SPI时序配置成了Mode 3而SD卡实际工作在Mode 0数据采样沿错位导致读到的全是垃圾。CubeMX里重新检查CPOL和CPHA确保是CPOL0、CPHA0。5.4 能读不能写或者写后校验失败能读不能写先检查卡侧面写保护开关是不是锁上了这是最容易被忽略的硬件原因。如果开关没锁再检查写流程里的忙等待是否完整。我见过很多代码问题出在写完一扇区后没有等待MISO回到0xFF紧接着发下一条命令卡还在忙状态命令被丢弃或数据错乱。写后立刻回读校验失败还有一个原因是数据响应字节判断错误。卡对写数据返回的响应是“xxx00101”低5位等于0x05才表示接受。如果把0x05以外的值当成成功比如把0xFF当成功就会误判。我的建议是在调试阶段把数据响应字节也返回给上层打印出来对比。5.5 FatFs挂载失败提示找不到文件系统如果底层扇区读写完全正常但f_mount返回FR_NO_FILESYSTEM说明卡上的第一个扇区不是合法的FAT引导扇区。最常见原因是卡之前被做成过启动盘或者系统安装盘里面有多个分区电脑上只显示其中一个分区而FatFs默认按卡的第0扇区解析自然失败。解决办法很简单把卡插读卡器在电脑上用磁盘管理器删除所有分区然后新建一个FAT32分区并格式化。注意选择FAT32不要选exFAT很多老版本FatFs和部分卡片对exFAT支持不好。格式化后重新插到STM32上f_mount就能顺利通过。5.6 换卡后不稳定如何应对SD卡SPI模式兼容性总体不错但确实会遇到个别卡在18MHz下不稳定。遇到这种情况可以把提速步骤的目标分频系数从4改成8也就是9MHz传输牺牲一点速度换稳定性。我的项目里封了一个SD_SPI_SetSpeed函数允许运行时动态切换速度排查疑难问题时可以先降到低速验证再逐步提速。如果是工业环境或者设备长期运行建议在SD卡方案里加一个“初始化失败自动重试”机制比如初始化失败后延迟100ms重新执行整个SD_Init流程连续3次都失败才报告错误。普通消费级卡片偶尔上电初始化失败是正常的重试大多能解决。写在最后一点实操中的个人体会最后分享一个我自己踩过最深坑的教训有一版代码死活初始化不过去卡在CMD0换了三张卡都一样最后发现是CubeMX初始化SPI时不小心把SCK引脚配置成了普通推挽输出而非复用推挽输出导致SCK根本没有时钟信号。从那以后我养成了一个习惯几乎每次调SPI都用逻辑分析仪先抓波形确认SCK有脉冲、MOSI有数据、CS在命令期间拉低再去看协议细节排查效率比盲改代码高好几倍。如果你只是想快速存日志、保存参数不建议一上来就写文件系统先跑通裸扇区读写再挂FatFs每一步都做个小验证整个过程其实不需要一个小时。这套方案后续还能扩展的方向很多比如用DMA进一步降低CPU占用、通过CMD18/CMD25做多扇区连续传输提高吞吐、用SD卡做小型文件服务器等。先把底层稳打稳扎调通后面随便怎么包装都很顺手。
返回列表