STM32F103 SPI驱动SD卡与FatFS移植实现USB读卡器
2026/9/16 11:43:12 网站建设 项目流程

简介:一套基于STM32F103 Blue Pill开发板的SPI接口SD卡读卡器示例工程,面向嵌入式开发者和单片机爱好者,可用于快速实现SD卡底层驱动与读写功能。包内共171个文件,以96个C源文件和60个头文件为主,涵盖HAL库初始化、SPI通信、SD卡命令交互等核心代码,辅以工程配置文件、链接脚本及Markdown说明,整体仅1.87MB,结构精简,便于直接导入System Workbench编译调试。已有1188人浏览学习,适合需要参考STM32 SPI外设用法或移植SD卡驱动的学习者。通过该工程可明确硬件接线要点(如CS接PA4、SPI引脚分配及USB上拉处理),理解CubeMX生成代码的模块组织方式,并基于现成驱动快速验证读写时序、排查常见初始化问题,为后续扩展文件系统或数据存储应用提供基础。

1. Blue Pill 的 SPI 接线与 SD 卡串行模式适配

STM32F103 的 Blue Pill 最小系统板大概是国内玩 SPI SD 卡成本最低的 ARM 平台,没有之一。这个项目要做的事情很明确:把一张 microSD 卡挂在 STM32F103 的 SPI1 上,通过 FatFS 读写文件,最终按 USB 读卡器的方向留好接口。核心思路是不走 STM32 的 SDIO 控制器,而是用 SPI 协议去驱动 SD 卡——SD 卡规范本身保留了 SPI 模式作为串行通信的退路,代价是吞吐率上限从 SDIO 的 48MB/s 掉到 2MB/s 左右,换来的是绝大多数 Cortex-M 单片机都能直接驱动,连引脚都不需要重映射。适合还在用标准库或 HAL 库打基础、想搞明白 SD 卡初始化时序和 FatFS 移植细节的人。读完这篇,你应该能在不过度依赖 SDIO 的前提下,从逻辑分析仪抓出来的波形判断卡是否认卡、命令是否被正确响应。

2. SPI 物理层与 SD 卡 SPI 模式接线

2.1 SPI 协议四线定义与时钟极性选择

SD 卡 SPI 模式的数据通路复用标准 SPI 主机接口,四根线分别是片选 CS、时钟 SCK、主机输出 MOSI、主机输入 MISO。和常见的 SPI NOR Flash 一样,SD 卡 SPI 模式没有数据就绪引脚,主机判断卡忙态的途径只有 MISO 引脚的电平状态和命令响应字节,这一点在后面做初始化时非常关键。

时钟极性方面,SD 卡规范规定 SPI 模式支持 CPOL=0、CPHA=1 和 CPOL=1、CPHA=1 两种采样方式(即 SPI Mode 0 和 Mode 3),STM32F103 的 SPI1 时钟极性配置里对应 Low 和 High 两个选项。我惯用 Mode 0,也就是 CPOL=0、CPHA=0,原因是 Mode 0 在时钟空闲时为低电平,大部分 SD 卡分线板上的电平转换芯片对空闲电平的要求更宽松,调试时用逻辑分析仪解析也省事。这里有个容易踩的坑:如果把 CPOL 写成 0、CPHA 写成 1(Mode 1),字节采样点会恰好落在 SD 卡输出端尚未拉稳的窗口,表现出来就是命令发出去以后读回的 R1 永远是 0xFF,貌似卡没反应,其实是采样相位错了。

时钟极性参数在 STM32CubeMX 中初始化代码里对应SPI_InitTypeDefCLKPolarityCLKPhase两个字段。使用 HAL 库时,这两项直接决定 SCK 空闲电平和数据线上的锁存边界,修改后要重新做一遍物理层测试,不能只改参数不确定是否真的切换生效。

2.2 硬件片选与软件片选差异

表 2-1 SPI1 引脚分配与复用功能

引脚功能方向说明
PA4SPI1_NSS / GPIO输出手动控制 CS,默认高电平
PA5SPI1_SCK输出APB2 72MHz 分频后输出约 18MHz 上限
PA6SPI1_MISO输入接收卡返回的 R1 响应和数据块
PA7SPI1_MOSI输出发送命令帧、0xFF dummy 字节
PA11USB_DM双向USB 差分数据负端
PA12USB_DP双向USB 差分数据正端,需外接上拉电阻

GPIO_InitTypeDef 的复用模式要设置为 GPIO_AF0_SPI1,但片选引脚绝不能交给 SPI 外设的硬件 NSS 管理。SPI_InitTypeDefNSS字段设为SPI_NSS_Soft之后,SPI1 的 NSS 引脚才释放给 GPIO 使用,否则硬件自动拉低片选会导致 SD 卡把一次连续的 SPI 命令流拆成多个孤立的字节,卡根本不认这种跨片选的命令帧。软件片选的核心操作是,在发送命令帧的第 1 个字节前拉低 CS,在收到响应最后一个字节后、且卡退出忙态之前再拉高 CS。

#define SD_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define SD_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

这段宏定义放在 sd_card.h 里,所有命令发送函数都依赖它。常见做法是 CS 引脚初始化时输出高电平,防止上电瞬间卡被误选中。如果工程里同时启用 SPI2 或软件模拟 SPI,CS 引脚可能被挪到 PB12 等其他管脚,那只需要改宏即可,命令链路不受影响。

2.3 供电顺序与上电时序

SD 卡规范对电源上电顺序有明确要求,先给 VDD 再拉 CS 为高。在 Blue Pill 板上,SD 卡分线板如果直接由板载 3V3 供电,AMS1117 的输出电流余量不大,遇到旧卡瞬态电流超过 100mA 时容易掉电复位,表现为初始化偶尔成功、读写大文件时卡死。稳妥做法是分线板单独接 5V 输入,板上 LDO 稳压到 3.3V 以后给卡供电,主控与卡之间共地。

上电完成后,SD 卡处于 SD 模式还是 SPI 模式由主机决定:在 CS 拉低的状态下发送至少 74 个时钟周期的 0xFF,然后发送 CMD0,卡才会切换到 SPI 模式。这里有个细节,74 个时钟周期不能用 HAL 库的延时函数代替,必须调用HAL_SPI_Transmit发送一串 0xFF 字节,而且发送期间 CS 必须保持高电平。初始化完成后,系统最好读一次 SD 卡的 CSD 寄存器,确认卡的容量类型和写保护位状态,避免后面 FatFS 挂载时因块大小不匹配而报错。

3. CubeMX 下 SPI1 最小配置与命令收发函数

3.1 低速时钟分频的 CubeMX 参数

在 STM32CubeMX 中新建 STM32F103X8 工程,RCC 的 HSE 选择 Crystal/Ceramic Resonator,Clock Configuration 里配到 72MHz 主频。SPI1 挂载在 APB2 总线上,APB2 分频器设为 1 时 SPI1 时钟源为 72MHz,CubeMX 的 SPI1 参数面板中 Baud Rate Prescaler 选择 256,对应 281.25kHz,满足 SD 卡初始化阶段不高于 400kHz 的时序约束。高速阶段把 Prescaler 改成 4,得到 18MHz 时钟,这是 STM32F103 SPI1 在 72MHz APB2 下的上限,刚好低于 SD 卡 SPI 模式标称的 25MHz 上限。

表 3-1 CubeMX SPI1 参数配置

配置项低速初始化高速读写
ModeTransmit Only MasterFull-Duplex Master
Hardware NSS SignalDisableDisable
Prescaler2564
Clock Polarity (CPOL)LowLow
Clock Phase (CPHA)1 Edge1 Edge
CRC CalculationDisableDisable
Data Size8 Bit8 Bit

CubeMX 生成的MX_SPI1_Init()里,hspi1.Init.BaudRatePrescaler在编译期被固定,运行期想切换 400kHz 和 18MHz 两种速率,需要重设BaudRatePrescaler后调用HAL_SPI_Init,或者直接在初始化时用 256 分频,读写时再重新初始化一次。更简便的做法是写一个SD_SPI_SetSpeed(uint8_t fast)函数,内部对hspi1.Instance->CR1的 BR 位做位操作,不重新初始化整个外设。修改时注意,BR 位只能在 SPI 关闭(SPE=0)状态下写入,否则行为未定义。

由 CubeMX 生成的工程会包含stm32f1xx_hal_spi.cstm32f1xx_hal_uart.c等源文件,即使只用 SPI1 读 SD 卡,HAL 库也会把 RCC 时钟树里使能过的外设驱动全部编译进去。这和使用 ST 标准外设库 v3.5 的旧工程风格不一样,标准库是把互不相关的外设源文件都加进工程,HAL 则按宏裁剪,实际链接时只有需要的函数会进固件。

3.2 命令帧发送与响应读取

SD 卡 SPI 模式是半双工的命令-响应模型,但物理层用全双工 SPI 实现,这意味着主机发送 6 字节命令帧的同时,卡在 MISO 线上回数据。发送每个字节时,主机要同步接收 MISO 上的返回字节;空闲时发 0xFF 维持 SCK 时钟。因此 HAL 库最好调用HAL_SPI_TransmitReceive成对收发,少用单向HAL_SPI_Transmit,否则响应字节会残留在移位寄存器里,最终导致连续读取时多出一个垃圾字节。

static uint8_t SD_xchg(uint8_t byte) { uint8_t rx; HAL_SPI_TransmitReceive(&hspi1, &byte, &rx, 1, 100); return rx; } void SD_WriteCommand(uint8_t cmd, uint32_t arg, uint8_t crc) { uint8_t frame[6]; frame[0] = 0x40 | (cmd & 0x3F); frame[1] = (arg >> 24) & 0xFF; frame[2] = (arg >> 16) & 0xFF; frame[3] = (arg >> 8) & 0xFF; frame[4] = arg & 0xFF; frame[5] = (crc << 1) | 0x01; for (int i = 0; i < 6; i++) SD_xchg(frame[i]); } uint8_t SD_ReadR1(void) { uint8_t resp; for (int i = 0; i < 8; i++) { resp = SD_xchg(0xFF); if (!(resp & 0x80)) break; } return resp; }

frame[0]的高两位固定为 01,即 0x40 加上命令号;arg是高字节在前的四字节参数;第 6 字节是 CRC7 和停止位。SD 卡进入 SPI 模式后,除 CMD0 和 CMD8 外的命令可以忽略 CRC,CRC 字段填 0 即可,但停止位必须为 1。SD_ReadR1里的循环是为了跳过 MISO 线上的 0xFF 前导字节,读到最高位为 0 的字节才认定是有效 R1 响应,这个时序细节在逻辑分析仪上看得非常清楚。

HAL_SPI_TransmitReceive的超时参数设为 100ms,初始化阶段卡配合慢速时钟,响应时间可能接近几十毫秒,超时设短了会把正常响应误判为无响应。如果调试中发现 R1 响应偶尔丢失,而且逻辑分析仪显示收到 0xFF,就用 250ms 的超时值把这种慢速卡的边界情况吞掉。

4. SD 卡初始化序列与 R1 状态机判定

4.1 CMD0 与 CMD8 的强制时序

SD 卡初始化是 SD 卡 SPI 模式中最容易出问题的阶段。上电后,主机要在 CS 高电平状态下发送至少 74 个时钟周期的 0xFF,然后拉低 CS,发送 CMD0(参数 0,CRC 固定 0x95)。CMD0 的响应是 R1 的 0x01,表示卡进入 IDLE 状态。假如 R1 返回 0x00,说明卡没有进入 SPI 模式,多半是初始化时的 CS 电平或时钟个数不对。

CMD8 用于区分 SDSC 和 SDHC/SDXC 卡。命令参数固定为 0x000001AA,CRC 为 0x87,响应是 R7,包含 4 字节的返回内容。如果 R7 低 12 位不等于 0x1AA,则该卡不兼容当前电压范围,可以直接放弃初始化。一些老 SDSC 卡不支持 CMD8,收到 CMD8 后 R1 返回 0x05(非法命令),此时要跳过 CMD8,继续走 CMD55/ACMD41 流程。

R1 响应的低 7 位含义值得背下来,0x01 表示 IDLE、0x02 表示擦除复位、0x04 表示非法命令、0x08 表示 CRC 错误、0x10 表示擦除序列错误、0x20 表示地址错误、0x40 表示参数错误。实际调试中,初始化循环最常见的失败值是 0x04 和 0x40,前者一般是卡不识别主机发的命令版本,后者是 ACMD41 的 OCR 参数填错或 HCS 位置不对。

uint8_t SD_Init(void) { uint8_t resp, i; SD_CS_HIGH(); SD_SPI_SetSpeed(0); for (i = 0; i < 10; i++) SD_xchg(0xFF); SD_CS_LOW(); SD_WriteCommand(0, 0, 0x95); resp = SD_ReadR1(); if (resp != 0x01) return SD_ERROR; SD_WriteCommand(8, 0x000001AA, 0x87); resp = SD_ReadR1(); if ((resp & 0x04) == 0) { for (i = 0; i < 4; i++) SD_xchg(0xFF); } for (i = 0; i < 200; i++) { SD_WriteCommand(55, 0, 0); resp = SD_ReadR1(); if (resp & 0x80) continue; SD_WriteCommand(41, 0x40000000, 0); resp = SD_ReadR1(); if (resp == 0x00) break; HAL_Delay(2); } if (i == 200) return SD_ERROR; SD_WriteCommand(16, 512, 0); resp = SD_ReadR1(); if (resp != 0x00) return SD_ERROR; SD_SPI_SetSpeed(1); return SD_OK; }

ACMD41 是应用特定命令,必须先发 CMD55 再发 ACMD41,两段命令之间不能插入其他命令。参数 0x40000000 里的 bit30 是 HCS 位,置 1 表示主机支持高容量卡,只有支持 SDHC/SDXC 的主机才允许置位。初始化循环里读resp & 0x80是为了过滤 MISO 上的忙信号,卡在 OCR 电源爬升阶段会回 0x01,就绪后回 0x00,这里最容易犯的错误是给 ACMD41 设定了 200ms 超时,结果碰上慢速老化卡需要 1 秒以上就绪,建议把循环周期放宽到 300 次、延时保持 2ms 不变。

4.2 读块与忙检测的时序边界

单块读取用 CMD17 实现,多块读取用 CMD18,两者都遵循同样的数据令牌机制:命令发送完成后,主机持续发 0xFF,直到 MISO 读到 0xFE 起始令牌,然后连续读 512 字节数据和 2 字节 CRC。如果读到 0x00 或 0x09 这类错误令牌,说明卡返回错误,数据无效。等待令牌的超时次数不能太少,物理扇区跨边界时有些卡会花几百微秒做内部搬移,逻辑分析仪上能看到这段时间 SCK 异常地长时间拉低。

uint8_t SD_ReadSector(uint8_t *buf, uint32_t sector) { uint8_t resp, token; uint32_t i; SD_CS_LOW(); SD_WriteCommand(17, sector << 9, 0); resp = SD_ReadR1(); if (resp != 0x00) { SD_CS_HIGH(); return SD_ERROR; } for (i = 0; i < 1000; i++) { token = SD_xchg(0xFF); if (token != 0xFF) break; } if (token != 0xFE) { SD_CS_HIGH(); return SD_ERROR; } for (i = 0; i < 512; i++) buf[i] = SD_xchg(0xFF); SD_xchg(0xFF); SD_xchg(0xFF); SD_CS_HIGH(); return SD_OK; }

sector << 9是把块地址转换成字节地址,SDSC 卡的地址空间是字节编址,SDHC 卡则是块编址,二者地址计算方法不同。这段代码默认卡能被 CMD58 读取 OCR 寄存器,并通过 CCS 位判断是块寻址卡还是字节寻址卡。如果没有 CMD58 判定,最好在初始化时读取 CSD 寄存器的 CSD_STRUCTURE 字段,根据卡容量类型确定地址左移位数,避免插入大容量卡时读写到错误扇区。

5. FatFS 文件系统移植与 UART 打印验证

5.1 diskio 层函数对接

FatFS 的移植核心是 diskio.c 里的六个函数:disk_initializedisk_statusdisk_readdisk_writedisk_ioctl。对读卡器这种只读场景,disk_write可以返回RES_WRPRTdisk_ioctl至少要实现GET_SECTOR_COUNTGET_BLOCK_SIZE两个功能,前者给 f_mkfs 用,后者给 f_open 分配簇时做对齐参考。

DSTATUS disk_initialize(BYTE pdrv) { if (pdrv != 0) return STA_NOINIT; if (SD_Init() == SD_OK) return 0; return STA_NOINIT; } 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(buff + i * 512, sector + i) != SD_OK) return RES_ERROR; } return RES_OK; }

FatFS 的 LBA_t 类型默认是无符号 32 位整数,SDXC 卡超过 2TB 不需要考虑,STM32F103 场景下 32GB 以内的卡配合_MAX_SS = 512已经足够。disc_read里每次读一个扇区,多扇区读取只是循环调用,性能上不如CMD18多块读,但胜在逻辑简单,不容易因为跨区边界产生错误。等初始化稳定后,可以把多块读优化成一次 CMD18 连续读,能减少一半以上的主机开销。

ffconf.h 里有几个参数和这个工程直接相关。_USE_MKFS置 1 以后才能在系统里调用 f_mkfs 格式化新卡;_USE_FASTSEEK置 1 会启用 FAT 簇缓存,对读卡器这种顺序读文件为主的场景提升不明显,但对小文件随机访问有明显帮助;_FS_READONLY置 1 可以裁掉文件写功能,减少 ROM 占用。这些参数在 CubeMX 的 FatFS 中间件配置面板里都能直接勾选,但要注意_MAX_SS千万不能设成 1024,因为 SD_ReadSector 的缓冲区是按 512 字节设计的。

5.2 挂载分区与从 SD 卡读取文件

初始化完成后,挂载文件系统的顺序是 f_mount -> f_open -> f_read。FatFS 的 f_mount 只注册工作区,真正读取分区表要等到第一次 f_open 或 f_getfree 时才发生。如果 SD 卡没有有效分区表或没格式化,f_mount 返回FR_NO_FILESYSTEM,这时需要调用 f_mkfs 重新格式化。读卡器产品的常见要求是出厂时用 PC 格式化好的卡插入后可识别,那么工程里可以在 f_mount 失败后自动 f_mkfs,但这类操作写扇区期间掉电容易损坏 SD 卡,量产时慎重。

FATFS fs; FIL file; UINT bytes_read; char line[128]; f_mount(&fs, "", 1); FRESULT res = f_open(&file, "config.txt", FA_READ); if (res == FR_OK) { f_read(&file, line, sizeof(line) - 1, &bytes_read); line[bytes_read] = '\0'; f_close(&file); HAL_UART_Transmit(&huart1, (uint8_t *)line, bytes_read, 1000); }

f_open 返回FR_OK后,读到的内容要主动补\0结尾,FatFS 不保证缓冲区末尾有字符串终止符。HAL_UART_Transmit 用于把读取结果回传到串口调试器,串口 1 和串口 3 的使用差异在 F103 上主要体现在时钟源不同:USART1 挂在 APB2,USART3 挂在 APB1,HAL_UART_Init的波特率计算结果会受 APB 分频影响,如果沿用默认 8MHz 的 HSI 时钟,串口 3 的实际波特率会和预期相差 3% 以上,码片对上后打印仍会出现乱码。

5.3 文件系统扇区对齐与卡格式的关系

很多人在 STM32F103 读 SD 卡时遇到 f_mount 挂载失败,排到后面发现是卡格式化时选了 exFAT 而不是 FAT32,FatFS 的_FS_EXFAT配置项默认是 0。读卡器固件里如果不开 exFAT 支持,就只能用 FAT32 格式的卡,Partition Table 里的 MBR 分区类型字段会写着 0x0C,用读卡器接到 PC 上仍然能正常识别。写操作也是同理,f_open 以写模式打开文件时,如果卡分区是 FAT32 但簇大小是 32KB,单次 f_write 的写入缓冲区和 SD 卡的 512 字节扇区对齐不一致,内部会先读改写,这就是文件写损坏事故的主要来源。

6. 生产级技巧:逻辑分析仪判包、速度梯度与 USB MSC 扩展

SD 卡 SPI 初始化失败不要直接怀疑卡坏,先用逻辑分析仪抓 MOSI 和 CS 上的波形。重点看三个位置:CS 拉低之前 SCK 上有没有至少 74 个周期的高电平;CMD0 帧的 6 个字节是否完整连续,帧内 CS 有没有意外拉高;MISO 线上回应的 0x01 是否落在命令帧结束后的 8 个时钟周期内。在 400kHz 速率下,一个字节占 2.5 微秒,逻辑分析仪采样率 16MHz 足够看清每一位边沿。处理这类问题时我常把 CubeMX 的 SPI1 速率切到最低,拉长单字节时间,方便肉眼逐位对比 SD 卡规范里的时序图。

初始化成功后切高速也要讲究梯度。400kHz 直接跳 18MHz 会让部分老卡内部 PLL 来不及锁定,表现为连续读 20 个扇区后某一个扇区返回超时。常见做法是先把 Prescaler 改到 16(4.5MHz),读一个扇区确认 R1 正常,再改到 4(18MHz),这样能过滤掉一批兼容性差的杂牌卡。SPI 模式跑到 18MHz 时,PCB 上如果 SD 卡座到 MCU 之间有 10cm 以上的长线,波形会明显振铃,一个字节里的第 6 到第 8 位就可能被采样错误,此时 MISO 线上串联 33Ω 电阻可以压制过冲。

如果卡能读扇区但 FatFS 挂载失败,优先检查卡的分区表而不是怀疑 SPI 时序,用一个独立的HC_ReadSector(0, buf)函数读 MBR 扇区,打印前 512 字节的十六进制内容,看偏移 0x1BE 处有没有 0x80 引导标志和 0x55 0xAA 结束标志。卡偶发读写错误时读 CSD 寄存器的 PERM_WRITE_PROTECT 位,这个位若为 1,即使卡侧边锁扣没有拨到写保护位置,也会拒绝写入,这就是网上经常讨论的“卡没锁却写保护”一类现象的来源之一。

整个读卡器方案的最后一段是 USB MSC。STM32F103 的 USB 设备控制器挂在 PA11/PA12 上,PA12 需要外接 1.5kΩ 上拉电阻到 3.3V,让主机在枚举时识别到设备连接。PA11 和 USART1 的 CTS 复用,如果调试串口用 PA9/PA10 就不会冲突,但把 USART1 的其它功能引脚映射到 PA11 会直接影响 USB 数据线,这是 F103 上一个非常隐蔽的引脚冲突,SD 卡 SPI 和 USB 共用工程时务必检查 GPIO 复用配置。USB MSC 类实现里把 FatFS 的disk_readdisk_write接到 USBD_MSC 中间件的读写回调上,主机通过 BULK 传输发起 SCSI READ 命令时,固件把 LBA 换算成 SD 卡扇区号,走的就是前面调通的 SPI 链路。USB 枚举要求 48MHz 时钟源,在 RCC 配置里要把 PLL 的 Q 分频打开后再倍频到 48MHz,这部分参数在 CubeMX 的 Clock Configuration 里和 SPI1 的 72MHz APB2 配在同一个时钟树,改动任何一路分频系数都可能让另一路超限。调试 USB 枚举时,把 SPI 时钟切回低速模式,能有效排除 USB 中断阻塞 SPI 传输导致的卡初始化失败。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询