ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103C8T6 SPI模式驱动SD卡完整教程

STM32F103C8T6 SPI模式驱动SD卡完整教程 先把结论放在前面STM32F103C8T6这颗芯片没有SDIO外设所以想让它读写SD卡最靠谱的方案就是走SPI。这个项目我调了近一天翻遍了各种资料和数据手册最后把一套能用的SPI模式SD卡驱动跑通了踩了不少电压、上拉、速率、命令时序的坑。这篇文章就把整个流程拆成5步从硬件接线、SPI初始化、SD卡初始化命令链到单扇区读写代码全部分享出来。不管你现在用的是标准库还是HAL库只要看懂了这套逻辑换到任何MCU上都是同一个套路。文章适合刚接触STM32的开发者也适合那些已经能点灯、跑串口但第一次碰SD卡、FatFS的同学。我会尽量把每条命令的作用、每个参数为什么要这么填、每段代码为什么要这么写讲清楚而不是丢一段能跑就完事的代码。1. 为什么SPI模式STM32F103C8T6跑SD卡的正确打开方式1.1 SD卡两种工作模式的差异SD卡从协议层面分为SD模式SDIO和SPI模式。SD模式是SD卡的本命协议数据线多、速度快、命令机制复杂需要MCU有SDIO外设支持。SPI模式则是SD卡规范为了兼容没有SDIO的芯片而保留的“简化模式”把SD协议的命令响应过程映射到四根SPI线上主机只需要一个普通的SPI外设就能驱动。这两种模式最大的区别在速度和使用门槛上。SD模式下SD卡可以跑4位并行数据总线理论传输速率能到几十MB/s级别而SPI模式是串行1位传输受限于SPI时钟频率。但在STM32F103C8T6这种资源级芯片上串行传输已经足够用了反而是SDIO外设缺失导致根本没有选择余地。C8T6只有64KB Flash、20KB RAM正常应用场景不外乎存传感器日志、存配置参数、存一些开机画面或者音频片段这些任务的数据量都不会太大。用SPI模式虽然跑不满SD卡的最高速度但胜在代码简单、逻辑清晰、调试方便非常适合作为学习和轻量级存储的起点。1.2 为什么在C8T6上默认选SPISTM32F103C8T6的数据手册里没有SDIO控制器这是芯片本身的外设资源限制。也就是说在C8T6这颗芯片上物理上就不存在SDIO引脚想用SDIO模式也没有硬件支持。这个约束直接决定了要在C8T6上读写SD卡只能走SPI或者用GPIO纯软件模拟SDIO协议。用GPIO模拟SDIO协议的做法我见过有人做出来过但实用性太差。SDIO协议比SPI复杂很多命令、响应、数据校验、多线并行时序全部要靠软件逐bit翻转电平一旦系统里还有中断、定时器、串口在跑时序就很容易飘调试成本极高。相比之下SPI模式只需要两个关键要素一个硬件SPI外设外加一根CS片选控制线。而且SPI模式还有额外的兼容性优势。SPI模式下SD卡的SPI时钟限制是25MHz而STM32F103C8T6的SPI1最高只能跑到18MHz所有速度档位都在SD卡允许范围内。哪怕用最保守的低分频系数也能稳定完成读写操作。对大多数嵌入式存储场景来说SPI模式是性价比最高、风险最低的路线。1.3 速度够用吗一个扇区的实际耗时估算很多人一听“SPI模式”就担心速度太慢。这里做个简单估算。SPI1挂在APB2总线上主时钟72MHz经过分频后最高18MHz。读一个512字节扇区理论上最少需要512×8/18M约等于227微秒再加上命令、响应、数据令牌、CRC的额外开销单扇区读取实测在300到400微秒左右。换算下来的持续读速度差不多1.5MB/s左右写速度会因为卡内部闪存擦写而降到1MB/s以下。这个速度放到具体场景里感受一下每秒写一条50字节的日志SPI模式绰绰有余一分钟录一段8kHz、16bit、单声道的PCM音频大概是1MB左右的数据量SPI模式也扛得住但如果要连续录制高清视频或者高速采集ADC数据那确实会成瓶颈。做项目之前先评估一下自己的数据吞吐需求C8T6配SPI模式适合的是数据记录、参数存储、资源文件读取这类场景。在此基础上明确一下资源分工SD卡占SPI1引脚就是PA5SCK、PA6MISO、PA7MOSI、PA4CS。这组引脚在最小系统板上都很容易引出完全不影响串口下载调试。2. 硬件接线与驱动基础别在接线细节上翻车2.1 最小系统板和SD模块的引脚分配SD卡模块的引脚一般是VCC、GND、CS、CLK、MOSI、MISO对应STM32F103C8T6的接线关系如下表所示我默认使用的是SPI1和软件片选。SD卡模块引脚STM32F103C8T6引脚说明VCC3.3V必须用3.3V供电GNDGND与MCU共地CSPA4GPIO推挽输出软件控制CLKPA5SPI1_SCK复用推挽输出MISOPA6SPI1_MISO浮空输入MOSIPA7SPI1_MOSI复用推挽输出很多同学的第一个坑出在PA4上。部分最小系统板的PA4默认被接了一个按键或者和其它外设共用了这时候软件片选会失灵卡初始化时怎么都不出0x01。上板之前一定先用万用表或者看原理图确认PA4是干净的或者干脆换一个没被占用的GPIO比如PB12改掉宏定义就行。2.2 供电、共地和上拉的三个硬性要求SD卡的工作电压是2.7V到3.6V绝大多数模块都是3.3V供电。有些模块板载了电平转换芯片可以接受5V输入但保险起见第一次上电前先看模块丝印或者资料确认VCC到底应该接5V还是3.3V再动手。如果拿不准统一接3.3V最安全。共地问题容易在面包板上翻车。STM32最小系统板和SD卡模块如果是两个独立的供电来源必须把GND连在一起。逻辑电平的参考点不一致SPI读回来的数据就是乱的初始化时表现为R1响应要么是0xFF要么是乱跳。上拉电阻是另一个容易被忽略的细节。SPI模式下SD卡的MISO线在空闲时不应该处于不确定电平但STM32的GPIO浮空输入本身没有内部上拉能力。我的经验是MISO线上加一个10kΩ上拉到3.3V尤其是用杜邦线和面包板时。没有上拉时卡在初始化阶段的R1响应经常是0xFE、0xFD这类奇怪的字节其实就是总线噪声被MISO采样到了。许多现成SD卡模块已经自带上拉但如果用裸卡座一定要补上。2.3 SPI外设初始化的关键参数Mode、分频和软片选STM32标准库里SPI1的初始化配置中有三个参数直接决定SD卡能不能通信成功。第一个是SPI_CPOL和SPI_CPHA必须组合成SPI模式0CPOL0CPHA1Edge或者SPI模式3CPOL1CPHA2Edge。我习惯用模式0也就是CPOL_Low、CPHA_1Edge只要保持一致初始化阶段用模式0切换高速后还保持模式0就不会有问题。第二个关键参数是分频系数。SD卡规范要求初始化阶段SPI时钟不能超过400kHz。72MHz主频下SPI1的分频至少要选SPI_BaudRatePrescaler_256得到约281kHz这是保证初始化成功的安全值。初始化完成之后再通过关闭SPI、修改BaudRatePrescaler、重新开启SPI的方式把速率提上去。第三个参数是SPI_NSS_Soft也就是软片选。这一个点能解释80%的SD卡读写异常。STM32的硬件NSS在SPI模式下自动控制片选而SD卡要求整个命令响应阶段CS保持低电平读取数据块期间也要保持低电平硬件NSS很难满足SD卡这个时序要求。所以片选必须用普通GPIO手动控制SPI_NSS设置为Soft让NSS引脚和SPI外设脱离关系。初始化函数我习惯写成下面这样上电时先把CS拉高防止SD卡误以为主机要发命令#define SD_CS_HIGH() GPIO_SetBits(GPIOA, GPIO_Pin_4) #define SD_CS_LOW() GPIO_ResetBits(GPIOA, GPIO_Pin_4) void SPI1_Init_SD(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); SD_CS_HIGH(); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_256; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial 7; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }注意SCK和MOSI配置为复用推挽输出MISO配置为浮空输入CS配置为普通推挽输出。这三个引脚的模式配错了后面的代码再正确也没有用。3. SD卡5步初始化流程每条命令都在干什么3.1 第一步上电延时与74个时钟脉冲SD卡上电后默认工作在自己的原生SD模式如果要切换到SPI模式主机必须先给卡一个明确的“信号”。具体做法是在CS引脚保持高电平的情况下SPI主机连续发送至少74个时钟脉冲也就是持续拉高MOSI、产生SCK时钟。因为CS是高电平SD卡在这个阶段只接收时钟不会把任何数据放到MISO上收到足够时钟后SD卡才会识别到总线被切换到了SPI模式。实际代码里通常发送10个字节的0xFF每个字节8个时钟共80个时钟比规范要求的74个多了6个够用了。这一步不能省很多初始化失败就是上电后直接发CMD0SD卡还没准备好接收SPI命令执行必然失败。等这80个时钟发完后再把CS拉低开始真正的命令阶段。CS拉低这个动作表示SD卡被选中后续的命令才会被SD卡接收。uint8_t SD_Init(void) { uint8_t r1; uint16_t timeout; SD_CS_HIGH(); for (uint8_t i 0; i 10; i) { SD_SPI_ReadWriteByte(0xFF); } SD_CS_LOW(); // 后续步骤依次执行 }3.2 第二步CMD0进入SPI模式CS拉低后第一条命令必须是CMD0参数为0作用是让SD卡复位并进入SPI模式。SD卡只有在SPI模式下收到CMD0才会从空闲状态开始响应。CMD0的返回响应R1应该是0x01代表卡已经进入idle状态。CMD0的CRC字段必须填对。在SD模式下CMD0的CRC7值是0x4A完整命令帧最后一个字节是0x95。虽然进入SPI模式后SD卡不再校验CRC但在还没有成功切换到SPI模式之前卡可能依然要求CRC正确。所以CMD0必须带上正确的CRC这一点是数据手册里写清楚的。CMD0发送后SD卡返回R1响应。R1是1个字节最高位为0表示有效响应低位的bit0为1表示卡处于idle状态。初始化阶段CMD0返回0x01就算成功如果超时没等到有效R1要重新拉高CS、补时钟、再拉低CS重试。r1 SD_SendCommand(0, 0, 0x4A); // CMD0参数0CRC70x4A if (r1 ! 0x01) return 1;3.3 第三步CMD8判断卡型和版本CMD8是SD 2.0协议引入的命令用于区分卡是SD 1.x老卡还是SD 2.0及以上版本的新卡同时检测SPI主机和卡之间的电压匹配。CMD8的参数固定填0x000001AA最后的AA表示主机支持2.7V到3.6V电压范围。CMD8的响应是R7长度为5个字节1字节R1后面跟4字节的卡返回值。如果卡支持SD 2.0R1返回0x01后面4字节应该回传0x00 0x00 0x01 0xAA。其中前两个字节是保留值第三个字节的bit0为1表示卡支持当前电压第四个字节回显0xAA。如果CMD8返回R10x05说明这是一张不识别CMD8的老卡初始化流程要切换到老卡分支直接用ACMD41并配合HCS0的参数。在SPI模式下CMD8的CRC其实卡不会校验但为了保证SD模式下的兼容性CMD8的CRC7值我直接按规范填0x08构造命令帧时最后一个字节用(crc 1) | 1生成的字节就是0x11。很多例程直接硬编码0x87实际也能过但按规范填更干净。uint8_t buf[4]; r1 SD_SendCommand(8, 0x000001AA, 0x08); if (r1 0x01) { for (int i 0; i 4; i) { buf[i] SD_SPI_ReadWriteByte(0xFF); } if (buf[2] ! 0x01 || buf[3] ! 0xAA) return 2; sd_type SD_TYPE_V2; } else { sd_type SD_TYPE_V1; }3.4 第四步ACMD41确认容量与初始化结束SD卡的初始化过程并不是发出一两条命令就立即完成的。卡收到ACMD41后会在内部执行上电、读CID、读CSD、准备接口等一系列操作整个过程需要一段时间。主机必须循环发送CMD55ACMD41直到ACMD41返回R10x00表示初始化完成。CMD55是“下一条命令是应用特定命令”的前导命令ACMD41才是真正的应用命令。CMD55参数填0ACMD41参数的bit30也就是HCS位需要根据卡的类型决定SD 2.0及以后版本的新卡填0x40000000让卡以SDHC/SDXC的方式初始化老卡则填0x00000000。初始化期间的ACMD41返回0x01是正常现象表示卡还在忙。不要一看到0x01就报错要循环等待。我一般设1000次超时每次命令中间间隔一些时钟周期实测绝大多数卡在几十次循环内就能完成初始化。如果超时后还是0x01大概率是上电时序、时钟速率、上拉电阻里有问题。timeout 1000; do { SD_SendCommand(55, 0, 0); // CMD55 r1 SD_SendCommand(41, 0x40000000, 0); // 新卡HCS1 } while (r1 ! 0x00 --timeout); if (r1 ! 0x00) return 3;3.5 第五步CMD58/CMD16收尾ACMD41初始化完成后卡已经可以正常响应命令但还需要确认SD卡的具体类型。CMD58是读OCR寄存器返回R3响应包含5个字节1字节R14字节OCR值。OCR的bit30如果是1说明卡是SDHC/SDXC类型后续读写命令里的地址直接使用扇区号如果是0说明是SDSC类型读写命令里的地址要乘以512换算成字节地址。这个容量类型判断直接决定后面所有读写命令的地址构造非常重要。SDHC卡如果按SDSC的字节地址方式访问读出来的数据全是0xFF反过来用扇区号去访问SDSC卡也会得到错误结果。我用一个全局变量记录卡类型后续所有读扇区和写扇区函数都判断这个变量。最后一条是CMD16设置块长度。SDSC卡支持可变块长度SDHC卡固定512字节。统一把块长度设为512字节让后续读写代码不用为不同卡做适配。r1 SD_SendCommand(58, 0, 0); if (r1 0x00) { for (int i 0; i 4; i) { buf[i] SD_SPI_ReadWriteByte(0xFF); } if (buf[0] 0x40) { sd_type | SD_TYPE_HC; } } SD_SendCommand(16, 512, 0);到这里SD卡初始化的5步就算全部走完了。整个命令序列的逻辑可以归纳成一句话先让卡从SD模式切到SPI模式再确认卡的类型和电压兼容性然后等待卡完成内部上电最后统一块长度。任何一步出错后面读写都不可能正常。4. 扇区读写代码实现从命令帧到数据块的完整链路4.1 底层字节收发函数怎么设计SPI协议是主从全双工主机每发送一个字节同时会从MISO上接收一个字节。SD卡在等待主机命令或发送数据时会监测总线上的电平主机则通过发送0xFF来产生时钟。因此底层字节收发函数本质上就是一个SPI读写函数发送0xFF就是读一个字节发送其他数据就是写一个字节。标准库写法如下发送前等待TXE标志发送后等待RXNE标志然后读取接收寄存器uint8_t SD_SPI_ReadWriteByte(uint8_t data) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendByte(SPI1, data); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveByte(SPI1); }这个函数是驱动里所有通信的基础。后面所有发送命令、读取响应、读取数据块、发送数据块的逻辑全部建立在它之上。HAL库用户只需要把SPI_I2S_SendByte替换成HAL_SPI_TransmitReceive就能复现同样效果。4.2 命令发送与R1响应读取SD卡的命令帧固定为6个字节。第一个字节的高6位是命令号bit5固定为1第2到第5字节是命令参数大端序第6字节是CRC7左移一位加上停止位1。uint8_t SD_SendCommand(uint8_t index, uint32_t arg, uint8_t crc) { uint8_t cmd[6]; uint8_t r1; uint8_t retry 8; cmd[0] 0x40 | index; cmd[1] (uint8_t)(arg 24); cmd[2] (uint8_t)(arg 16); cmd[3] (uint8_t)(arg 8); cmd[4] (uint8_t)arg; cmd[5] (uint8_t)((crc 1) | 1); SD_SPI_ReadWriteByte(0xFF); for (uint8_t i 0; i 6; i) { SD_SPI_ReadWriteByte(cmd[i]); } while (retry--) { r1 SD_SPI_ReadWriteByte(0xFF); if ((r1 0x80) 0) { return r1; } } return 0xFF; }读取R1响应是这个函数的关键。SD卡在收到命令后MISO上会先出现若干个0xFF然后才是有意义的R1字节。R1的最高位为0所以主机会循环读取MISO直到某个字节的最高位是0才认定这是有效R1。如果返回0xFF表示总线空闲要继续读一直读到超时都没等到有效R1说明命令执行失败。4.3 读取单扇区CMD170xFE令牌与CRC读单扇区的完整时序是CS拉低发送CMD17参数为扇区地址等待R10x00然后持续读取数据直到收到数据令牌0xFE后面跟着512字节数据加2字节CRC。CRC可以不校验直接跳过。地址参数的构造要注意初始化时如果识别出卡是SDHC类型CMD17的参数直接传扇区号即可SDSC类型就需要把扇区号左移9位换算成字节地址后再传给命令帧。这个判断放在函数内部调用者统一传扇区号函数内部自动做换算。等待数据令牌0xFE时需要设置超时。有些卡读取速度比较慢从R1返回开始到数据令牌出现可能要好几个毫秒如果只读几次就读不到0xFE就退出会造成偶发性的读取失败。uint8_t SD_ReadSector(uint32_t sector, uint8_t *buffer) { uint32_t addr; uint8_t r1; uint16_t timeout; uint32_t token_timeout; if (sd_type SD_TYPE_HC) { addr sector; } else { addr sector 9; } SD_CS_LOW(); r1 SD_SendCommand(17, addr, 0); if (r1 ! 0x00) { SD_CS_HIGH(); return 1; } token_timeout 50000; while (SD_SPI_ReadWriteByte(0xFF) ! 0xFE) { if (--token_timeout 0) { SD_CS_HIGH(); return 2; } } for (uint16_t i 0; i 512; i) { buffer[i] SD_SPI_ReadWriteByte(0xFF); } SD_SPI_ReadWriteByte(0xFF); SD_SPI_ReadWriteByte(0xFF); SD_CS_HIGH(); SD_SPI_ReadWriteByte(0xFF); return 0; }读完后CS拉高并补一个时钟目的是让SD卡进入空闲状态方便下一次命令。这个补时钟的动作很多人会漏掉但漏掉后偶尔会出现在同一张卡上连续读多个扇区时中间某个扇区响应变慢或读错的情况。4.4 写入单扇区CMD240xFE令牌与忙检测写单扇区的时序和读略有不同。发送CMD24等待R10x00后主机要主动发送一个数据起始令牌0xFE然后紧跟512字节数据和2字节CRC。CRC可以填0x00SD卡在SPI模式下不校验CRC。数据块发送完之后卡会将数据写入内部闪存这个阶段SD卡会让MISO保持低电平表示处于忙状态。主机必须持续发送0xFF来读取MISO直到MISO恢复到0xFF高电平才代表这一次写操作彻底完成。这个忙检测不能省。如果写完不等忙结束马上拉高CS或者发下一条命令写入结果往往是未知的最常见的就是读回来全是旧数据。uint8_t SD_WriteSector(uint32_t sector, const uint8_t *buffer) { uint32_t addr; uint8_t r1; uint16_t timeout; uint8_t status; if (sd_type SD_TYPE_HC) { addr sector; } else { addr sector 9; } SD_CS_LOW(); r1 SD_SendCommand(24, addr, 0); if (r1 ! 0x00) { SD_CS_HIGH(); return 1; } SD_SPI_ReadWriteByte(0xFE); for (uint16_t i 0; i 512; i) { SD_SPI_ReadWriteByte(buffer[i]); } SD_SPI_ReadWriteByte(0x00); SD_SPI_ReadWriteByte(0x00); timeout 50000; do { status SD_SPI_ReadWriteByte(0xFF); } while ((status ! 0xFF) --timeout); SD_CS_HIGH(); SD_SPI_ReadWriteByte(0xFF); return (status 0xFF) ? 0 : 3; }写完CS拉高同样要补一个空闲时钟。到这里最底层的单扇区读写驱动就完整了配合第3章的初始化流程已经可以进行最基本的“写一个扇区、读回验证”测试。5. 我踩过的坑与验证技巧初始化失败和读回乱码的排查顺序5.1 初始化卡死时钟、上拉、CRC的逐个排查初始化阶段最大的问题是R1响应始终是0xFF或者乱码。我调试时习惯用逻辑分析仪挂上CLK和MISO先看波形再改代码。排查顺序是这样的第一看SPI分频是否设置到了256分频确认时钟低于400kHz第二看MISO有没有上拉电阻浮空输入时总线噪声非常容易导致R1乱码第三看CMD0的CRC是否正确这个原因虽然少见但确实存在。如果CMD0能返回0x01但CMD8返回不对重点检查两个地方一个是参数是不是0x000001AA另一个是命令帧的字节顺序是不是大端。SPI发送时参数先发最高字节这个顺序错了卡完全看不懂。还有一个小坑是CMD8后要记得连续读4个字节的参数返回不读干净的话后面ACMD41会被残留字节干扰。如果卡在了ACMD41循环超时有一个很容易忽略的原因CMD55返回之后ACMD41的参数没有根据卡类型正确设置。老卡吃HCS0新卡吃HCS1。如果把老卡强行设成HCS1它可能一直不返回0x00。排查时可以先用串口把CMD55和ACMD41的响应都打印出来看是哪一步卡住。5.2 能读ID但读数据全0xFFSDSC和SDHC地址问题初始化成功能读到CID、CSD但读扇区时数据全是0xFF这是最典型的“卡类型判断错误”症状。CMD58返回的OCR寄存器bit30为1代表SDHC为0代表SDSC。如果代码里没有做这个判断或者判断逻辑出错SDHC卡会被当作SDSC访问命令里的地址变成字节地址但卡实际接收到的还是扇区号两边对不上读出来的自然全是0xFF。这个问题特别容易在代码移植时出现。网上很多例程写死字节地址只适配了SDSC卡。换一张SDHC卡就翻车。解决方式就是严格遵守第3章第5步的CMD58判断逻辑并在读写函数里根据卡类型自动换算地址。另外用一张已知容量的卡做验证很有帮助一张8GB的新卡基本都是SDHC如果判断结果不是SDHC那肯定哪里出了问题。5.3 读回数据不一致写后忙等待和数据块长度写扇区读回来数据不一致常见的两个原因分别是写后忙等待不足和数据块长度不匹配。写后忙等待不足会在连续写入时体现得更明显第一次写入看起来正常第二次写入后读回来还是第一次的内容。这是因为第二次写完立即发出下一次操作时卡还在忙主机发出的数据和命令被卡忽略了。解决方式就是严格检测忙状态忙状态未结束前不发送任何新命令。数据块长度问题主要出在SDSC卡上。CMD16把块长度设置为512后读写都必须按512字节操作。如果初始化时漏了CMD16SDSC卡默认块长度可能是512也可能是其它值写512字节进去会导致卡内部数据错位。SDHC卡没有这个问题因为SDHC固定512字节但为了代码统一无论什么卡都执行一次CMD16是最稳妥的做法。5.4 提速技巧DMA与FatFS对接思路初始化阶段用256分频的慢速跑通后可以把SPI时钟提上去。F103的SPI1最高支持18MHz但杜邦线长度超过10厘米时高速SPI的时钟边沿会出现明显的过冲和振铃。我的实测经验是18MHz在面包板上容易偶发读错降到9MHz后非常稳定。如果你的板子是PCB布线并且走线短18MHz可以尝试否则9MHz是甜点频率。提速只影响时钟速率不影响逻辑。把SPI1的BaudRatePrescaler改成SPI_BaudRatePrescaler_89MHz或SPI_BaudRatePrescaler_418MHz即可。想进一步提升性能读扇区时可以用DMA把512字节数据从SPI1_DR搬到内存缓冲区省掉每字节等待标志位的CPU开销。但DMA只是搬运数据命令发送、R1读取、0xFE令牌等待这些流程仍然要按轮询方式处理。如果要做文件系统直接移植FatFS是最快的路径。FatFS的底层映射只有六个函数disk_initialize、disk_status、disk_read、disk_write、disk_ioctl、get_fattime。其中disk_read和disk_write就是在这个驱动之上加一层循环把count个扇区逐个读出再搬运到FatFS缓冲区或者从FatFS缓冲区逐个写入SD卡。disk_ioctl里实现GET_SECTOR_COUNT和GET_BLOCK_SIZE两个功能后就能正常format和挂载了。DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv ! 0) return RES_PARERR; 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) { if (pdrv ! 0) return RES_PARERR; for (UINT i 0; i count; i) { if (SD_WriteSector(sector i, buff i * 512) ! 0) { return RES_ERROR; } } return RES_OK; }迁移到国产GD32、APM32这类兼容芯片时SPI外设寄存器和库函数几乎一致这套驱动基本不用改动就能编译通过只是SPI分频值要根据对应芯片的主频重新算一遍。最后再分享一个我的调试习惯拿到一块新卡先不着急挂文件系统就用裸扇区读写函数写一个固定pattern比如把扇区0到7填成0xAA、0x55交替然后读回来比对。这一关过了再挂FatFS后面就算出问题也知道是文件系统层还是物理层的问题。实际项目中很多莫名其妙的“卡坏了”最后排查下来都是初始化时序或地址换算的锅跟卡本身无关。
返回列表