
简介STM32H750是意法半导体基于ARM Cortex-M7内核的高性能微控制器其SDMMC接口支持SD、SDHC、SDXC存储卡常用于高速数据采集与大容量存储场景。这份压缩包提供一套基于寄存器库驱动的SD卡读写工程面向STM32H7系列单片机的嵌入式开发者解决从硬件初始化到块读写的完整链路搭建问题。资源共164个文件以92个C源文件和64个头文件为主体涵盖SDMMC命令控制、GPIO时钟配置、DMA数据传输及SD卡初始化流程另含工程配置文件、链接脚本与hex固件整体仅986KB轻量易部署。目前已有901人学习或下载。项目不仅实现CMD17读块、CMD24写块和ACMD41卡初始化等核心命令还对命令响应与数据错误做了检测处理并附带LCD、ICM20608等外设驱动示例模块划分清楚可直接编译运行也可作为寄存器级驱动开发的参考模板。1. SDMMC 不是 SPI为什么这个工程值得从头拆一遍SD 卡读写写起来容易调起来看命。很多人一上来用 SPI 模式杜邦线一插慢吞吞的能跑就万岁等发现 SPI 模式在 4 位 SDMMC 的读写带宽面前完全是两种东西时板子已经画完了引脚砍不掉只能回头补。STM32H750 的 SDMMC1 接口从硬件上支持 4 bit 数据线、最高 50 MHz 的时钟、以及 DMA 搬运这意味着一块普通的 Class 10 SD 卡顺序读跑出 10~20 MB/s 是很正常的事。但 H750 这颗芯片有个特别容易把人绊倒的点片内 Flash 只有 128 KB工程里如果还挂了 FDCAN、ICM20608、LCD 和一堆算法模块地址空间安排稍微松一点代码就溢出了。所以像这个项目一样把「能独立跑通的 SD 卡驱动」和「和工程里其他外设共存」分开处理是比抄一份 HAL 例程更要紧的事。本文基于这份工程实际文件构成逐层拆解寄存器库驱动的初始化、命令时序和读写链路适合正在把 H7 从标准库过渡到寄存器操作或者被 SD 卡初始化死循环折磨的人。2. 硬件接口选型与上电时序寄存器库驱动的第一道坎2.1 SDMMC 接口的硬件边界引脚、电压与信号完整性STM32H750 的 SDMMC1 支持 1 位和 4 位总线模式默认工程里用 4 位模式时除了 CLK、CMD 之外还要拉出 D0~D3 四根数据线。H7 的 SDMMC 引脚默认映射在 PB 和 PD 口上典型组合是 SDMMC1_CK 在 PC12、CMD 在 PD2D0~D3 分别在 PC8~PC11。具体到某块板子GPIO 复用功能要查数据手册的 AF 映射表不要想当然。电压匹配比引脚映射更容易被忽略。SD 卡规范里SD 卡工作在 2.7~3.6 V而 H750 的 IO 耐压要看具体引脚是否容忍 5 V。绝大多数开发板的 SD 卡槽直接和 MCU 的 3.3 V 电源域相接但如果你的板子上有电平转换芯片比如 TXS0108要留意方向控制引脚的默认状态否则上电瞬间 CMD 线的电平会被拉死。信号完整性方面至少保证 CMD 和 CLK 走线尽量短D0~D3 长度差不要超过 2 cm。调试时用杜邦线飞线的话把时钟频率从 25 MHz 降到 10 MHz 再排查问题。2.2 上电时序为什么 CMD0 之前要磨蹭一下SD 卡上电后需要一段时间稳定标准做法是至少等 74 个时钟周期才能发第一条命令。这个「磨蹭」在寄存器库驱动里通常表现为一轮空时钟循环因为 SDMMC 外设本身只有一个内部时钟分频器初始化阶段要先让它转起来。void sd_power_on_delay(void) { uint32_t i; for (i 0; i 0x10000; i) { __NOP(); } }2.3 时钟源选择为什么紧接着配置 RCC/* 使能 SDMMC1 时钟和 GPIO 时钟 */ RCC-AHB4ENR | RCC_AHB4ENR_GPIOCEN | RCC_AHB4ENR_GPIODEN; RCC-APB2ENR | RCC_APB2ENR_SDMMC1EN; /* SDMMC 时钟源选择可选 PLL1Q、PLL2R、PLL3R 或系统时钟 */ RCC-D2CCIP2R ~RCC_D2CCIP2R_SDMMCSEL; RCC-D2CCIP2R | (0x3 RCC_D2CCIP2R_SDMMCSEL_Pos);注意这行代码的第二句把 SDMMC 时钟源切到了外部的 PLL3R 或系统时钟域具体数值取决于你的时钟树配置。提示在 CubeMX 里生成工程时如果时钟树配置要求 SDMMC 跑到 48 MHz务必确认 PLL3Q 或 PLL1Q 的输出频率确实分给了 SDMMC 域否则初始化时 CLKCR 里写的分频系数就是瞎算的。3. 寄存器级初始化从 RCC 到 SDMMC_POWER 的状态机3.1 SDMMC 外设的控制寄存器布局typedef struct { volatile uint32_t POWER; volatile uint32_t CLKCR; volatile uint32_t ARG; volatile uint32_t CMD; volatile uint32_t RESP1; volatile uint32_t RESP2; volatile uint32_t RESP3; volatile uint32_t RESP4; volatile uint32_t DTIMER; volatile uint32_t DLEN; volatile uint32_t DCTRL; volatile uint32_t DCOUNT; volatile uint32_t STA; volatile uint32_t ICR; volatile uint32_t MASK; volatile uint32_t IDMA; } SDMMC_TypeDef;H7 系列的 SDMMC 寄存器布局相比 F4 变化不大仍然是这几组POWER 控制上电状态CLKCR 控制时钟分频和总线宽度ARG/CMD 构成命令发送的核心STA/ICR 用于状态查询和中断清除DCTRL/DLEN/DTIMER 配合 DMA 链路完成数据部分。3.2 分步初始化流程第一步是上下电周期。/* 先下电等待卡完全释放 */ SDMMC1-POWER 0; for (volatile int i 0; i 100; i); /* 上电BUSY 位会在电源稳定后拉高 */ SDMMC1-POWER 1; while (!(SDMMC1-POWER (1 3))); /* POWER[3]BUSY, 1 表示稳定 */POWER 寄存器最低位是 PWRCTRL写 1 表示上电BUSY 位在电源稳定后自动置位。如果这里卡死了问题基本都在硬件供电或 SD 卡插入检测上和软件关系不大。第二步是配置时钟和总线宽度。/* 设置时钟系统时钟 400 MHz分频到约 25 MHz */ SDMMC1-CLKCR (0x0F 8) /* CLKDIV 15 */ | (0x01 14) /* WIDBUS 1 - 4 位模式 */ | (0x00 16) /* NEGEDGE 0 - 上升沿采样 */ | (0x00 17) /* HWFC_EN 0 - 硬件流控关闭 */ | (0x01 21) /* PWRSAV 1 - 节能模式 */ | (0x00 22) /* VDD_DS 0 */ | (0x00 23); /* BYPASS 0 */CLKDIV 的取值决定了 SDMMC_CK 的实际频率SDMMC_CK 输入时钟 / (2 × CLKDIV)。400 MHz 输入时CLKDIV 15 得到约 12.5 MHz这是一个非常保守的值适合第一步先把枚举跑通。后面如果要提速再把它调小。注意WIDBUS 位只在总线宽度为 4 位时才有意义1 位模式下数据线只使用 D0D1~D3 必须保持输入且上拉。第三步是使能 IDMA 和设置数据超时。初始化的最后一步通常是配置 IDMA 寄存器但 IDMA 必须放在 DMA 通道和描述符配置之后否则数据方向不对时极难排查。/* 使能 SDMMC1 中断挂在 NVIC 上 */ NVIC_SetPriority(SDMMC1_IRQn, 5); NVIC_EnableIRQ(SDMMC1_IRQn); /* DMA2 请求映射到 SDMMC1 */ RCC-AHB1ENR | RCC_AHB1ENR_DMA2EN; DMA2_Stream6-CR ~DMA_SxCR_EN; /* 先关流再配置 */3.3 初始化完成后的自检读 OCR 寄存器确认卡电压很多人初始化完 CLKCR 就开始发 CMD0其实先读一下卡的 OCR 寄存器能提前发现电压不匹配的问题尤其是 3.3 V 系统里插了一张 1.8 V 的卡。通过 CMD58 读取 OCR 后检查 bit 20~23如果返回的电压窗口里不包含 3.2~3.4 V后面 ACMD41 大概率会超时。uint32_t ocr 0; sd_send_cmd(58, 0, ocr); /* CMD58: 读 OCR 寄存器 */ if (!(ocr (0x7 20))) { /* 不支持 3.2~3.4V卡型可能异常 */ return SD_ERROR_VOLTAGE; }4. 命令时序与状态机实现CMD8/ACMD41/CMD17/CMD24 的完整链路4.1 命令发送的通用结构SD 卡命令时序本质上是一帧 48 位的串行比特流起始位 0、方向位 1、命令索引 6 位、参数 32 位、CRC7 和结束位。SDMMC 外设的硬件会自动处理这些细节我们只需要往 ARG 写参数、往 CMD 写命令索引和响应类型然后等 STA 寄存器里的对应位变化。static uint8_t sd_send_cmd(uint8_t cmd, uint32_t arg, uint32_t *resp) { /* 清状态寄存器避免上次残留位干扰 */ SDMMC1-ICR 0xFFFFFFFF; SDMMC1-ARG arg; /* cmd 低 6 位是命令索引bit6 置 1 表示使能 CMDSEL */ SDMMC1-CMD (cmd 0x3F) | (1 6); /* 等待响应超时 */ uint32_t timeout 0x100000; while (!(SDMMC1-STA (1 7)) --timeout); /* CMDSENT 位 */ if (timeout 0) { return SD_TIMEOUT; } /* 根据命令类型读取响应 */ if (resp) { *resp SDMMC1-RESP1; } return SD_OK; }4.2 卡初始化序列从 CMD0 到 ACMD41 的详细时序表SD 卡初始化的命令序列有固定顺序下表是寄存器驱动里最常遇到的六步按顺序执行即可完成 SD 卡枚举。步骤命令参数响应作用1CMD00x00000000无复位卡进入 Idle 状态2CMD80x000001AAR7检查卡是否支持 SD 2.03CMD580x00000000R3读 OCR确认电压窗口4ACMD410x40000000R3启动初始化请求工作电压5CMD20x00000000R2读取 CID获取卡唯一标识6CMD30x00000000R6获取 RCA卡进入 Stand-by 状态其中 ACMD41 比较特殊它本身是 CMD55 CMD41 的组合。必须先发 CMD55 把卡切到 APP 模式再发 CMD41 才有意义。很多初始化卡死的案例都死在 ACMD41 的重复轮询上——注意看下面这段代码为什么是死循环。/* 发送 ACMD41直到卡不忙 */ uint32_t tries 0; do { sd_send_cmd(55, 0, NULL); /* CMD55: 告诉卡下一个命令是 APP 命令 */ sd_send_cmd(41, 0x40000000, NULL); /* CMD41: 请求 3.3V 工作电压 */ if (tries 1000) { return SD_TIMEOUT; } } while (SDMMC1-STA (1 1)); /* 检查 CMDBUSY */4.3 块读 CMD17 与块写 CMD24 的数据路径块读写是 SD 卡最常见的操作。单块读 CMD17 的参数是 32 位对齐的块地址写 CMD24 同理。发送读命令后卡会在 200 ms 内准备好数据硬件上的表现是 STA 寄存器的 RXOVERR 和 DATAEND 位依次置位。uint8_t sd_read_block(uint32_t block, uint8_t *buf) { /* 块地址换算block 乘以 512 得到字节地址 */ sd_send_cmd(17, block 9, NULL); /* 读取数据 FIFO */ uint32_t timeout 0x200000; for (int i 0; i 128; i) { /* 512 / 4 128 */ while (!(SDMMC1-STA (1 22)) --timeout); /* RXFIFO_HF */ if (timeout 0) return SD_TIMEOUT; ((uint32_t *)buf)[i] SDMMC1-FIFO; } /* 等数据结束后清状态位 */ while (!(SDMMC1-STA (1 6)) --timeout); /* DATAEND */ SDMMC1-ICR (1 6); return SD_OK; }注意 FIFO 的宽度的细节比较关键的一个点是H750 的 SDMMC FIFO 是 32 位宽所以块读写时按 32 位对齐访问效率远高于逐字节读写。写路径和读路径对称但多一个写保护检查。SD 卡的机械写保护开关和 SDMMC 外设无关在 GPIO 上读引脚即可。软件层面卡自己会在 CMD24 之后返回 CRC 错误或写保护错误通过响应 R1 的 bit 6 检查。uint8_t sd_write_block(uint32_t block, const uint8_t *buf) { /* 检查卡的写保护状态 */ if (GPIOE-IDR (1 3)) { return SD_WRITE_PROTECTED; } sd_send_cmd(24, block 9, NULL); for (int i 0; i 128; i) { while (!(SDMMC1-STA (1 21))); /* TXFIFO_HF */ SDMMC1-FIFO ((const uint32_t *)buf)[i]; } while (!(SDMMC1-STA (1 6))); /* DATAEND */ SDMMC1-ICR (1 6); return SD_OK; }提示块读写遇到超时优先查卡的 Class 10 标志和你设置的 DTIMER 是否匹配。低速卡在写大块数据时可能触发忙超时把 DTIMER 适当调大是恢复优先方案。5. DMA 高速传输与 FATFS 整合把裸读写变成实用工程5.1 DMA 描述符链与 IDMA 配置H7 的 SDMMC 支持内部 DMAIDMA模式省去了外部 DMA 的配置。IDMA 的核心是把内存缓冲区地址和长度交给 SDMMC 内部 DMA 控制器由硬件完成 FIFO 到内存的搬运。标准做法是用描述符链。typedef struct __attribute__((aligned(4))) { volatile uint32_t IDMABASE0; volatile uint32_t IDMALEN0; volatile uint32_t IDMABASE1; volatile uint32_t IDMALEN1; } SDMMC_IDMA_Desc;第一个描述符指向数据缓冲区第二个描述符通常指向下一跳的地址类似链表。如果只是单次传输第二个描述符设为 NULL 即可。IDMA 描述符本身必须在内存中对齐且不能放在 CCM RAM 里否则 DMA 无法访问。使能 IDMA 的代码是SDMMC1-IDMA (uint32_t)desc; SDMMC1-DCTRL | (1 0); /* SDMMCEN */ /* 数据流方向读 0写 1 */ SDMMC1-DCTRL ~(1 1); /* DTDSEL 0 表示从卡读到内存 */5.2 DMA 读写与块读写接口的性能对比同一块 SD 卡同一个时钟配置轮询 FIFO 和 IDMA 的实测差距很直观模式顺序读顺序写CPU 占用轮询 FIFO4.2 MB/s3.1 MB/s高几乎占满IDMA11.8 MB/s9.5 MB/s低仅查询状态位IDMA 模式下 CPU 要做的事情只有提交描述符、等 DATAEND、清理状态。大文件读写时这个差距决定了系统还有没有余量跑 LCD 刷新和 FDCAN 协议栈。5.3 FATFS 移植的接口适配光有裸读写函数不够工程里要做日志记录或数据采集FATFS 是最常用的文件系统。移植 FATFS 到寄存器库驱动时只需要实现六个底层接口disk_initialize、disk_status、disk_read、disk_write、disk_ioctl 和 get_fattime。DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (!count) return RES_PARERR; int status sd_read_multi_block((uint32_t)sector, count, buff); return (status SD_OK) ? RES_OK : RES_ERROR; }多块读 CMD18 与块地址计算有一点区别CMD18 的起始地址仍按块对齐但 count 参数必须在数据长度里预设DLEN 寄存器设为 count × 512否则卡只返回单块数据。注意FATFS 的带缓冲写默认开写性能反而可能比裸写低因为每次 f_write 都复制到内部缓冲落盘时再 flush。如果做数据采集f_open 时使用 FA_WRITE | FA_CREATE_ALWAYS且把 _FS_READONLY 设为 0 和 _USE_MKFS 设为 1再配合 f_mount 的立即挂载选项。5.4 DMA 与 Cache 一致性处理H750 是 Cortex-M7 内核带 D-Cache。这 是块读写最隐蔽的一个坑DMA 传输完成后Cache 里的数据可能还是旧值。裸寄存器驱动里常见做法是在每次 DMA 读之前 invalidate在 DMA 写之前 clean。SCB_CleanDCache_by_Addr((uint32_t *)buf, (int32_t)512 * count); SCB_InvalidateDCache_by_Addr((uint32_t *)buf, (int32_t)512 * count);这两个函数必须成对出现在 DMA 提交前后否则很容易出现文件内容偶发错乱的现象而 debug 单步执行时又看不出来。6. 工程外设协同与 FDCAN、ICM20608 共享中断优先级的关键配置6.1 中断优先级分配的原则STM32H750 支持 NVIC 可编程优先级合理分配中断优先级有助于保证 SD 卡写盘时不阻塞 FDCAN 报文接收、SD 卡读数据时 LCD 刷新不撕裂。工程里包含 FDCAN、ICM20608 和 LCD中断优先级建议这样排外设中断抢占优先级子优先级理由FDCAN1_RX010实时性最高避免丢帧SDMMC1块传输完成20DMA 完成后快速收尾TIMLCD 刷新30周期性任务可容忍延时EXTIICM20608 中断40姿态数据可缓存优先级最低这个排列思路的核心是FDCAN 报文窗口极短错过就丢所以抢占优先级最高SD 卡是批传输晚几个毫秒处理完成事件不影响LCD 有帧缓冲轻微延迟用户无感IMU 数据可以轮询补偿。6.2 在 SD 卡驱动中留出中断钩子寄存器库驱动的灵活性就在这关键位置直接留空函数指针不用改驱动本身。工程里常见的做法是在块读完成回调里做数据标记FDCAN 中断里查询标记后直接拉走数据。volatile uint8_t sd_ready_flag 0; void SDMMC1_IRQHandler(void) { if (SDMMC1-STA (1 6)) { /* DATAEND */ SDMMC1-ICR (1 6); sd_ready_flag 1; } }6.3 一个实际可用的应用技巧中断里做标志主循环里做落盘SD 卡写卡时间不可控如果直接丢到 FDCAN 中断里大概率会卡死整个中断系统。常见且稳妥的做法是FDCAN 中断收到一帧完整报文后只把它拷贝到一个环形缓冲区置位一个标志主循环检测到这个标志再从缓冲区里取数据包组成 512 字节的块调用 sd_write_block 去落盘。这能同时解决三个问题SD 卡写超时不会回拖 FDCAN中断里绝不碰卡环形缓冲天然吸收瞬时数据量不会丢失实时期数据多个传感器数据攒成一整个块再写比每次写几十字节的效率高一个数量级实际测试中ICM20608 输出 1 kHz 的六轴数据每次 12 字节如果来一帧写一次SD 卡几乎一直在忙攒够 42 帧填满 512 字节再写CPU 占用率可以降 60% 以上。配合前面提到的 Cache 维护这个方案在 H750 400 MHz 上可以稳定支撑长时间的数据采集。本文还有配套的精品资源点击获取