资讯详情

STM32H750寄存器级SD卡读写:SDMMC驱动与FATFS移植实战

📅 2026/9/17 12:30:11 | 华诺云谱 👁 阅读
STM32H750寄存器级SD卡读写:SDMMC驱动与FATFS移植实战
简介面向STM32H7系列嵌入式开发者这份资源基于寄存器库驱动实现了STM32H750的SD卡读写适合需要直接操控SDMMC接口、在高速数据采集或大容量存储场景下二次开发的工程师参考。压缩包共164个文件、986KB以92个h头文件和64个c源文件为主另有uvprojx工程文件、ld链接脚本、hex固件等头文件与源码模块划分清晰便于按需移植和裁剪。代码覆盖SD卡检测、初始化、块读写、DMA传输和错误处理等关键环节板块间按SDMMC、GPIO、RCC、DMA寄存器配置分层组织可直接在STM32H7系列单片机编译运行也可作为理解SD卡CMD17/CMD24读写时序与寄存器库开发的落地模板。当前已有901人浏览学习适合希望绕过HAL抽象层、深入底层寄存器逻辑的嵌入式开发者。1. STM32H750做SD卡读写为什么要绕开HAL和标准库手上有H750开发板的工程师大多遇到过这种情况片外Flash里固件跑得好好的一插上SD卡想存点采集数据程序却卡死在SDMMC初始化里。H750片内Flash只有128KB代码跑到片外之后SD卡几乎是量产设备里最常用的落盘介质。但H7系列的SDMMC外设和F4时代完全不同HAL库的抽象又给中断加了一层调度延迟数据吞吐越往上提问题越难查。这篇文章用寄存器操作方式实现SD卡读写不依赖HAL或标准库直接把SDMMC外设的寄存器行为讲清楚。寄存器方案适合两类人一类在做固件升级、数据记录这类对时序敏感的功能另一类想把SDMMC驱动彻底吃透好在H750、H743以及后续H7型号之间快速迁移。下面从SDMMC外设的初始化开始逐步完成卡识别、块读写、FATFS移植和排错验证。2. SDMMC初始化与卡识别把SDIO时钟从400kHz提到50MHz2.1 H7的SDMMC不是F4的SDIOSTM32H750内部有两个SDMMC外设SDMMC1和SDMMC2挂在AHB1总线上。每个外设只有一组寄存器空间寄存器布局在H7全系列里基本一致这也是标题里强调“支持STM32H7系列单片机”的直接原因。换型号时只要改RCC时钟使能位和引脚映射表寄存器驱动本身不需要动。先看一个关键差异F4的SDIO外设时钟分频公式是SDIO_CK SDIOCLK / (2 × CLKDIV)CLKDIV为0时输出SDIOCLK / 2。H7的公式则不同当CLKDIV为0时SDMMC_CK 内核时钟不做分频。也就是说H7的寄存器库驱动不能照搬F4的初始化代码否则上电后SDIO时钟可能是极高的满频直接击穿SD卡允许的25MHz上限。H7的SDMMC内核时钟通常来自PLL1Q配合AHB预分频后得到。比如AHB1运行在200MHzSDMMC内核时钟也是200MHz那么初始化阶段要让SDMMC_CK低于400kHzCLKDIV寄存器就要写很大。识别阶段把CLKDIV设为2输出50MHz适合SD高速模式若卡不稳定改CLKDIV为3降到33MHz。以下是CLKDIV对应的实际卡时钟速以200MHz内核时钟为基准这部分务必均匀分布CLKDIV值SDMMC_CK 200MHz / (2 × CLKDIV)适用场景2500xFA约400kHz上电识别阶段250MHzSDHS高速卡读写333MHz兼容线长、做在底板上的卡座6约16.7MHz长排线连接信号完整性差时兜底CLKDIV寄存器位于SDMMC_CLKCR的bit 0~5这是6位字段上限为63。识别阶段用250要用0xFA写入如果寄存器字段只有6位那么这里的写法需要小心实际上H7的CLKDIV字段是6位最大63250写不进去。需要调整计算方式要产生低于400kHz可以用内核时钟50MHz给SDMMC1专门分频然后CLKDIV63时得到约392kHz或者用200MHz时钟CLKDIV250超不过63就写不了。所以更稳妥的做法是先给SDMMC外设配一个较低的内核时钟或者直接把初始化时钟分频设为0xFA再考虑字段溢出问题。这块正是H7寄存器驱动容易踩的第一个坑CLKDIV字段宽度不够。我一般给SDMMC选一个独立的时钟源让它运行在100MHzCLKDIV125会溢出。实际工程里更常见的做法是让SDMMC内核时钟运行在50MHz识别阶段CLKDIV63得到约396kHz识别完成后CLKDIV0输出50MHz这样代码逻辑最自然。初始化的核心代码如下直接操作RCC和SDMMC寄存器。这里用了一个最小结构体描述外设基址没有引入CMSIS头文件。#define SDMMC1_BASE 0x48000000UL #define RCC_BASE 0x58024400UL #define RCC_AHB3ENR (*(volatile uint32_t *)(RCC_BASE 0x0D4)) #define GPIOB_BASE 0x48020400UL typedef struct { volatile uint32_t POWER; // 0x00 volatile uint32_t CLKCR; // 0x04 volatile uint32_t ARG; // 0x08 volatile uint32_t CMD; // 0x0C volatile uint32_t RESP1; // 0x10 volatile uint32_t RESP2; volatile uint32_t RESP3; volatile uint32_t RESP4; volatile uint32_t DTIMER; // 0x28 volatile uint32_t DLEN; volatile uint32_t DCTRL; volatile uint32_t DCOUNT; volatile uint32_t STA; // 0x38 volatile uint32_t ICR; volatile uint32_t MASK; volatile uint32_t FIFOCNT; volatile uint32_t FIFO; // 0x80 } SDMMC_Regs; #define SDMMC1 ((SDMMC_Regs *)SDMMC1_BASE) void sdmmc_clk_init(int div) { // 使能SDMMC1时钟和GPIOB时钟 RCC_AHB3ENR | (1 6); // SDMMC1时钟使能具体位见参考手册RCC_AHB3ENR // GPIOB时钟使能代码省略按具体型号配置 // 关闭外设电源然后重新上电 SDMMC1-POWER 0; for (volatile int i 0; i 1000; i); SDMMC1-POWER 1; // 使能SDMMC电源 SDMMC1-CLKCR (div 0x3F) | (1u 8); // CLKDIV 使能时钟输出 }这段代码对应H7上电后最基本的SDMMC配置。CLKCR的bit 0~5是CLKDIVbit 8是CLKEN时钟使能位。识别阶段调用sdmmc_clk_init(63)卡识别完成后再调用sdmmc_clk_init(0)。SDMMC1基址在H750参考手册RM0393中为0x48000000SDMMC2基址为0x48002000做双卡板时直接替换基址宏即可。2.2 卡识别流程的寄存器实现SD卡上电后必须按固定顺序走完卡识别流程这个流程由SD规范定义任何单片机做SD卡读写都无法绕过。寄存器驱动和HAL库的区别在于每个命令的发送、响应接收、状态位判定都要自己控制。卡识别分三步走顺序不能颠倒发送CMD0进入空闲态发送CMD8确认SD V2.0协议支持再循环发送ACMD41让卡完成上电初始化。int sd_cmd_send(uint8_t cmd_idx, uint32_t arg, uint8_t resp_type) { // 等待上一条命令完成 while (SDMMC1-STA (1 6)); // CMDBUSY置位时等待 SDMMC1-ARG arg; // CMD寄存器bit0使能命令bit6~0是命令索引(实际为bit21~24附近按H7手册修正) // 注意H7的CMD寄存器格式为 bit21 CMDSEN, bit22 WAITRESP, bit23 WAITINT uint32_t cmd_reg (cmd_idx 0x3F) 24; // CMDINDEX在bit24~29 if (resp_type 1) cmd_reg | (1u 6) | (1u 22); // 短响应 else if (resp_type 2) cmd_reg | (1u 6) | (1u 22) | (1u 23); // 长响应 SDMMC1-CMD cmd_reg | 1u; // 使能命令发送 // 等待响应寄存器就绪或超时 uint32_t timeout 1000000; while (!(SDMMC1-STA (1 2)) timeout--) {} // CTIMEout位判断 if (timeout 0) return -1; // 清除命令相关状态位 SDMMC1-ICR (1 2) | (1 1) | (1 0); // 清CTIMEOUT/CMDSENT/CMDREND return 0; }这里需要澄清H7寄存器库驱动的CMD寄存器位域定义上面代码中cmd_idx左移24位是用的是SDIO早期寄存器格式H7的SDMMC CMD寄存器实际为bit 21 CMSDENbit 22 WAITRESPbit 23 WAITINTbits 28~24 CMDINDEX。因此重新修正命令发送逻辑int sd_cmd_send(uint8_t cmd_idx, uint32_t arg, uint8_t need_resp) { while (SDMMC1-STA (1 6)); // 等待CMDBUSY释放 SDMMC1-ARG arg; uint32_t cmd_reg ((uint32_t)cmd_idx 0x1F) 24; // CMDINDEX bit24~28 cmd_reg | (1u 21); // CMSDEN使能命令状态机 if (need_resp) { cmd_reg | (1u 22); // WAITRESP等待短响应 } SDMMC1-CMD cmd_reg; // 写1到CPSMEN启动命令 uint32_t timeout 1000000; // STA bit0 CMDREND或bit2 CTIMEOUT任一置位表示命令阶段完成 while (!(SDMMC1-STA 0x05) timeout--) {} if (timeout 0) return -1; SDMMC1-ICR 0x07; // 清除CMDREND/CTIMEOUT/CMDSENT return 0; }这个函数是所有SD卡命令发送的基础。CMD8、CMD17这些命令只要调用它就能完成发送和响应等待。注意SD卡的ACMD是“先发CMD55再发真正的命令”所以ACMD41在代码层面是两条普通命令不必单独封装一个ACMD函数。卡识别的完整序列如下这一段逻辑直接决定卡是否被正确初始化。很多卡死出现在ACMD41超时本质是VDD电压窗口设置不对。void sd_card_init(void) { sdmmc_clk_init(63); // 慢速时钟保证识别稳定 for (volatile int i 0; i 10000; i); // 至少74个时钟周期这里用延时代替 sd_cmd_send(0, 0, 0); // CMD0进入空闲态 sd_cmd_send(8, 0x1AA, 1); // CMD8询问V2.0支持响应低8位为0xAA则支持 uint32_t hcs 0; uint32_t status 0; int retries 100; do { sd_cmd_send(55, 0, 1); // CMD55告诉卡下一条是应用命令 hcs 0x40000000; // HCS1表示主机支持大容量卡 sd_cmd_send(41, hcs, 1); // ACMD41启动初始化 status SDMMC1-RESP1; } while (!(status (1 31)) retries--); // 位31是卡忙标志 // 读取OCR判断卡容量类型 sd_cmd_send(2, 0, 2); // CMD2读取CID sd_cmd_send(3, 0, 1); // CMD3读取RCA进入Standby状态 // 卡识别完毕提高时钟到50MHz sdmmc_clk_init(0); // CLKDIV0输出50MHz // 可选CMD7选中卡RCA从RESP1中取bit16~31 }这段初始化流程对应SD规范里的上电识别状态机。CMD2的响应是136位长响应分布在RESP1~RESP4四个寄存器里实际用不到CID内容时不用读取。CMD3返回的RCA存在RESP1的bit16~31后续CMD7选卡时要用这个值。注意ACMD41的HCS位必须在CMD8响应成功且电压匹配后才置1如果是老式SD1.x卡CMD8会超时此时要改用HCS0重新走ACMD41。完整的代码里需要保存一个sd_card_type变量来区分SDSC和SDHCFATFS的扇区寻址会用到这个信息。3. 块读写命令与数据搬运CMD17/CMD18/CMD24的状态位检查3.1 单块读与多块读的寄存器差异初始化完成后卡进入传输状态。单块读是CMD17多块读是CMD18两者的区别只在数据结束阶段。多块读需要发送CMD12来中止数据传输否则卡会一直处在“发送数据”状态。H7的SDMMC外设提供了自动结束模式寄存器驱动不必手动维护状态机。数据方向的控制集中在DCTRL寄存器。读数据时bit 14 DTDIR为1写数据时bit 14为0。每次数据操作前必须设置DTIMER超时值和DLEN数据长度两者缺一不可。下面以单块读512字节为例展示寄存器级别的实现int sd_read_block(uint32_t sector, uint8_t *buf) { uint32_t addr sector 9; // 扇区号转字节地址SDSC卡需要乘512 // 配置数据超时和长度 SDMMC1-DTIMER 0xFFFFFFFF; // 超时值设最大覆盖低速卡 SDMMC1-DLEN 512; // DCTRL: DTDIR1(读), DTMODE1(块模式), DTBUFACK0, DTEN1 SDMMC1-DCTRL (1u 14) | (1u 12) | (1u 0); // 发送CMD17 sd_cmd_send(17, addr, 1); // 等待FIFO有数据。STA bit10 RXFIFOHF表示接收FIFO半满 uint32_t timeout 1000000; while (timeout--) { if (SDMMC1-STA (1 10)) { // RXFIFOHF uint32_t words SDMMC1-FIFOCNT; // 当前FIFO中32位字数 while (words--) { uint32_t data *(volatile uint32_t *)SDMMC1-FIFO; memcpy(buf, data, 4); buf 4; } } if (SDMMC1-STA (1 5)) break; // DATAEND数据已全部接收 // 检查接收溢出错误 if (SDMMC1-STA (1 15)) { SDMMC1-ICR (1 15); return -1; } } if (timeout 0) return -1; SDMMC1-ICR (1 5) | (1 11); // 清DATAEND和RXFIFOHF return 0; }这段代码的要点在FIFO读取方式。H7的SDMMC数据FIFO是32位宽的每读一次FIFO寄存器自动消费掉4字节。FIFOCNT寄存器指示当前剩余多少32位字必须在循环里递减读取。如果一次性往buf里拷4字节需要保证buf地址按4字节对齐否则用memcpy做非对齐拷贝会多消耗几个周期但对H750的Cortex-M7来说属于可接受范围。轮询方式在单块读时表现良好DMA方式在多块读时才有明显优势。单块场景下轮询的确定性反而更好不会被DMA中断延迟干扰。需要小心的是STA bit5 DATAEND的清除时机必须在读完所有FIFO数据后再清不能提前清否则残留数据还没来得及读出就丢了。3.2 多块写实现与卡忙等待多块写是CMD25数据长度可以预设为多个512字节块。与读数据不同写数据时主机要主动向FIFO里填充数据且SD卡写后有一个内部编程过程主机必须等卡从“忙”变“闲”否则下一笔写入会失败。int sd_write_multi(uint32_t sector, uint8_t *buf, uint32_t block_cnt) { uint32_t addr sector 9; uint32_t total_len block_cnt 9; SDMMC1-DTIMER 0xFFFFFFFF; SDMMC1-DLEN total_len; // DTDIR0(写), DTMODE1(块模式), DTEN1 SDMMC1-DCTRL (1u 12) | (1u 0); sd_cmd_send(25, addr, 1); uint32_t remaining total_len / 4; // 总字数 uint32_t timeout; while (remaining) { // 等待TXFIFO空标志。STA bit9 TXFIFOE表示发送FIFO空 timeout 1000000; while (!(SDMMC1-STA (1 9)) timeout--) {} if (timeout 0) return -1; while (!(SDMMC1-STA (1 13)) remaining) { // TXFIFOLT低水位 *(volatile uint32_t *)SDMMC1-FIFO *(uint32_t *)buf; buf 4; remaining--; } } // 等待数据发送完成 timeout 1000000; while (!(SDMMC1-STA (1 5)) timeout--) {} if (timeout 0) return -1; // 等待卡退出忙状态。STA bit8 SDIOIT不适用用DCOUNT判断或延时 for (volatile int i 0; i 100000; i) {} SDMMC1-ICR 0xFFFF; // 清除所有状态位 return 0; }多块写最容易出问题的地方不在命令发送而在FIFO写下溢。当主机写入FIFO的速度慢于SDMMC向外发送的速度会产生TXUNDERR错误表现在STA bit14。调试时如果发现这个错误位置位需要把FIFO水印阈值调低或者减少每两次FIFO写入之间的代码逻辑。上面代码用TXFIFOLT低水位标志配合剩余字节数循环就是为了保证FIFO始终保持有数据的状态。DCTRL寄存器的bit 11~10是FIFO水印阈值配置H7默认配置在1/2位置写操作时建议调整为1/4留出更多余量。这个参数在HAL库里默认是写死的寄存器驱动才有机会按卡的实际速度调整。3.3 命令响应检查和卡状态轮询的配合块读写命令发送后除了R1响应还需要查看卡当前状态在R1响应的bit9~12。SD卡规范定义了卡在不同状态下可以接受的命令。比如CMD17只能在传输状态tran下使用如果卡还处在Standby状态就要先发CMD7选中它。寄存器驱动不主动做这个判断时卡会直接返回错误响应代码稍不留神就当超时处理了。一个易用的调试技巧每次发送命令后把RESP1寄存器的值保存下来R1响应的bit8是错误位bit9是地址错误bit13是卡被锁定。这些位在FATFS出现“卡无法挂载”问题时能直接指出原因比盲改FATFS配置有效得多。uint32_t r1 SDMMC1-RESP1; if (r1 (1 8)) { // 通用错误位 // 打印错误码可配合UART调试 }SD卡读写驱动到这一步已经具备完整的块设备能力。接下来要把这些块操作接口接到FATFS上让用户用f_open、f_write这类文件操作代替直接访问扇区。4. FATFS与diskio接口把扇区读写封装成文件系统操作4.1 移植FATFS时的diskio函数要求FATFS是一个独立于硬件的文件系统层它通过disk_read、disk_write、disk_initialize、disk_status和disk_ioctl五个函数与底层设备交互。寄存器库驱动只需要把第2节、第3节实现的SD卡操作填进这几个函数里FATFS就能正常挂载。diskio接口位于ff.c调用的底层移植时最核心的是disk_read和disk_write的参数语义。FATFS传入的sector是逻辑扇区号count是要读写的扇区数缓冲区buff指向用户数据。这与SD卡CMD17/CMD18的寻址单位一致但有一个类型隐蔽的坑FATFS的BYTE数组地址不一定按4字节对齐而SDMMC FIFO要求至少32位对齐。解决方式有两种。第一种是强制要求应用层在ffconf.h中开启FF_USE_LUN并固定缓冲区对齐在MDK工程里用__attribute__((aligned(4)))声明缓冲区。第二种是内部再拷贝一次在disk_read里准备一个512字节的静态对齐缓冲。后者牺牲了性能但换来了极大的应用层自由度。static uint8_t align_buf[512] __attribute__((aligned(4))); DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (pdrv ! 0) return RES_PARERR; uint32_t addr sector; while (count--) { if (((uint32_t)buff 0x03) ! 0) { // 缓冲区未对齐先读到对齐缓冲区再拷贝 if (sd_read_block(addr, align_buf) ! 0) return RES_ERROR; memcpy(buff, align_buf, 512); } else { if (sd_read_block(addr, buff) ! 0) return RES_ERROR; } buff 512; addr; } return RES_OK; }这个实现里sd_read_block接收的addr是字节地址还是扇区号取决于第3节怎么写。SDHC卡采用块寻址扇区号可以直接作为CMD17的参数SDSC卡采用字节寻址需要左移9位。FATFS层面拿到的都是扇区号所以判别工作应该放在底层完成disk_read把它们统一转换成块地址传入SD卡驱动应用层不感知这些差异。disk_ioctl是另一个容易漏掉的函数。FATFS挂载时会调用CTRL_SYNC在做好disk_write后一定要返回RES_OK否则f_mount直接报FR_DISK_ERR。定义如下完整接口ioctl命令含义寄存器驱动实现CTRL_SYNC确认数据写完检查SDMMC_STA的DATAEND位GET_SECTOR_COUNT返回卡总扇区数读取CSD寄存器容量字段GET_SECTOR_SIZE扇区大小返回512GET_BLOCK_SIZE擦除块大小从CSD中提取或返回1DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case CTRL_SYNC: return RES_OK; case GET_SECTOR_COUNT: *(DWORD *)buff sd_get_sector_count(); 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.2 FATFS的配置参数与H750片外App共存设备挂载FFAT卷时要注意片外Flash和SD卡的主从关系。H750的片外App做固件升级时SD卡可能同时存放固件包和日志文件这会产生频繁的两类文件读写。这种情况下ffconf.h里的FF_USE_FASTSEEK建议开启可以显著加快在日志文件末尾追加写的速度。还有一个参数是FF_USE_LFNH750片内RAM充足但片外Flash的程序跳转和静态变量位置需要注意LFN缓冲区改为堆分配能减少全局数组占用。FATFS读写SD卡时较容易出现“挂载失败但块设备读写正常”的现象。问题多出在disk_ioctl没有正确返回卡总扇区数或者返回的扇区数不是512的整数倍。此时可以先在main里只调用disk_read读0号扇区用十六进制打印前8个字节。如果读到的是EB 58 90开头的引导扇区说明底层SD驱动没问题问题在FATFS层。部门同事在调试H750片外App卡死时常见思路是怀疑片外Flash读取时序但逻辑分析仪抓下来发现复位后程序一直在SD卡初始化里循环。卡识别阶段延时不足、上电时序不满足74个时钟周期都会让ACMD41一直返回忙。给卡座供电引脚上并联一个100uF电解电容能解决相当一部分偶发卡死问题这个细节对热插拔场景很重要。4.3 性能参数随手查FATFS配置参考表基于寄存器驱动的SD卡读写最终能跑多远由ffconf.h配置和时钟分频共同决定。下面是H750配合SD高速卡时一组经过多个项目验证的配置组合配置宏值说明FF_FS_EXFAT1支持exFAT处理大视频文件更快FF_USE_FASTSEEK1打开文件快速定位FF_USE_LFN1长文件名缓冲区放堆里FF_USE_MKFS1保留格式化功能量产时用FF_USE_TRIM1向卡发送TRIM命令延长寿命FF_USE_FIND0不需要目录枚举就关闭FF_VOLUMES1只挂载一个SD卡卷这个表格里TRIM命令值得留意。SD卡长期写日志文件会产生大量无效块TRIM可以主动通知卡回收这些块。FATFS在删除文件时会调用disk_ioctl的CTRL_TRIM对应的底层实现是对CMD35/CMD36的封装比较简单。最后一步是把diskio和FATFS一起编译链接。不要忘记ffconf.h里的FF_MIN_SS和FF_MAX_SS都设置为512否则FATFS会在f_mount时校验扇区尺寸返回FR_INT_ERR错误。整个SD卡读写驱动到这里就闭合了。5. 调试三板斧卡死定位、速度实测与波形核查5.1 三个高频卡死场景的定位顺序片外App卡死这个现象在H750嵌入式设备里被反复提及。当程序表现为“插入SD卡后死机”优先用GPIO翻转标记法定位不要上来就调FATFS。在SD卡初始化的每个关键节点后翻转一次一个空余GPIO用示波器测量两个翻转沿之间的时间间隔。如果CMD0和CMD8之间间隔达到毫秒级说明卡响应异常或上电复位不充分。初始化阶段卡死的三个高频原因按优先级排查上电时序不满足规范SD卡需要供电稳定后至少74个时钟周期才能接收CMD0。寄存器驱动在使能SDMMC时钟后立刻发命令很容易因卡侧初始化未完成而失败。CMD8响应丢失卡是SD1.x老卡时不会响应CMD8。此时代码不能把CMD8超时当成致命错误要走ACMD41的HCS0分支。CLKDIV配置错误初始化阶段时钟超过400kHz会直接导致卡连续出错。CLKCR设置后第一时间读回验证有时写0x3F会被其他位干扰。数据传输阶段的卡死排查方向不同。块读卡死通常是FIFO读取不及时导致RXOVERR溢出块写卡死是FIFO写出过快导致TXUNDERR下溢。两者在STA寄存器里的标志位不同中断里把STA值完整保存下来靠标志位区分比用万用表量电平可靠得多。5.2 用GPIO翻转测SD卡实际读速度寄存器驱动比HAL库慢很多的情况偶有发生多数原因是FIFO轮询代码里每到FIFO半满就查询一次STA状态位判断本身消耗了大量CPU周期。一个简易测速方法是给写操作加时间戳DWT-CYCCNT 0; DWT-CTRL | 1; // 使能DWT计数器 uint32_t start DWT-CYCCNT; sd_write_multi(0, test_buf, 128); // 写64KB数据 uint32_t cycles DWT-CYCCNT - start; // 主频480MHz时64KB耗时 cycles / 480000000 秒这个方法不需要额外接线可以在任意情况下估算吞吐量。如果算出的速度低于卡标称值的40%优先检查CLKDIV是否真的配置到了0然后确认GPIO速度等级是否设置为GPIO_SPEED_HIGH。H7的GPIO速度设置过低时信号上升沿变缓卡端采样错误率上升表现为偶发数据错误而非完全卡死。5.3 性能优化的最后一步多块DMA搬运寄存器驱动的SD卡写入速度上限通常在5~8MB/s瓶颈在于FIFO轮询写入。想突破这个上限要把数据搬运交给DMA实现。H7的DMA支持MDMA到AHB总线可以直接访问SDMMC的FIFO地址。开启DMA后写操作配置变为DCTRL的bit 4 DTEN置1然后使能DMA请求DMA自动完成FIFO到内存的数据搬运。一个隐蔽的坑是DMA完成中断并不代表SD卡写操作结束必须等STA的DATAEND位置位才能认为数据已经发出。DMA搬运完成的是“数据进FIFO”SDMMC还在继续向外发送二者之间存在时间差。设计中断处理时先后顺序要严格保持先等DMA传输完成再等DATAEND最后等卡退出忙状态任何一步提前都会造成数据错乱。DMA方式下缓冲区要求必须是32位对齐之前disk_read里的对齐检查代码可以保留既满足CPU轮询场景也兼容DMA场景。这一步完成后SD卡读写性能接近SDMMC外设的理论上限寄存器库驱动的调优工作才算真正收尾。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。