尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

STM32H743 SD卡读写实战:MDMA+FATFS配置避坑与性能优化

发布时间:2026/9/24 14:01:07

资讯中心
01
ARTICLE

STM32H743 SD卡读写实战:MDMA+FATFS配置避坑与性能优化

STM32H743 SD卡读写实战:MDMA+FATFS配置避坑与性能优化
1. 从“点灯顺利”到“SD卡写崩”的落差STM32H743这颗芯片性能拉满的时候是真的猛主频480MHz带FPU和DP-FPU跑起来比不少Linux级别应用的单片机都利索。但正是这颗芯片让我在SD卡读写这件事上栽了差不多一个礼拜的跟头。如果你也以为“不就是SD卡嘛F103上都跑过FATFSH743照抄总行吧”那我劝你冷静一下。先说结论STM32H743的SDMMC外设配合CubeMX生成的代码走MDMA搬运数据再挂上FATFS这套组合在配置上不算难真正难的是那些“配置了也不报错、跑起来就出鬼”的隐性坑。我这次把整个过程从头到尾复现了一遍从CubeMX的引脚配置、时钟树设置、MDMA通道选择到FatFs的底层适配再到实际跑读写性能测试每一步都记录下来了。这篇文章主要写给打算在H7系列上做数据记录、音频采集、大容量存储这类项目的朋友尤其是刚从F1/F4平台迁过来的人看完应该能帮你少走至少三天的弯路。这次用的硬件平台是一块自制的H743核心板SD卡座是标准的microSD push-push型供电用3.3V LDO信号线上拉了上拉电阻SD卡用的是一张闪迪的16GB Class 10卡。软件环境是STM32CubeMX 6.10 IAR 9.30HAL库版本是1.11.1。下面直接进入正题。2. CubeMX配置阶段最容易埋雷的几个地方2.1 引脚分配别让PCardDetect占错位置CubeMX里配置SDMMC1常规做法是把SDMMC1的4位数据线、时钟线和命令线都分配给固定引脚然后勾选SD Card Detect引脚用来检测SD卡是否存在。这一步看着简单但很多人第一步就埋了雷。H743的SDMMC1引脚映射CK是PC12CMD是PD2数据线是PC8到PC11这几个是硬件固定的没得选。卡检测引脚就灵活了CubeMX默认会给你分配一个PC6或者其他任意可用引脚但这里有个问题——你选的这个引脚必须能容忍3.3V电平并且实际板子上这个引脚确实连到了卡座的CD开关上。很多人图省事直接用了CubeMX自动分配的引脚结果板子上根本没引这个信号出来或者引到了5V容忍引脚上但没做电平匹配最终就是系统检测不到卡。我这次在原理图阶段就把PC6分配给了SD卡座的CD引脚CubeMX里在GPIO设置中把它配置为输入模式并使能内部上拉。为什么一定要内部上拉因为microSD卡座的CD开关在插入卡时通常是闭合到地的不插卡时悬空内部上拉可以保证逻辑电平稳定。如果你用的卡座是常开型的那就需要下拉这个务必对照你自己板子的原理图确认不要想当然。2.2 时钟树H7的SDMMC时钟不是你想的那么简单H743的SDMMC时钟源有PLL1Q、PLL2P、PLL3R、SYSCLK等多种选择CubeMX默认通常会选PLL1Q。这里我要重点提醒SDMMC的最高工作时钟在H743上有一个硬性上限就是52MHz但实际上有很多卡在超过48MHz之后就工作不稳定了。很多从F4平台过来的人习惯性把SDMMC时钟拉满。F4时代SDIO的时钟是48MHz大家都这么干没问题。H743虽然标称最高支持52MHz但实测下来Class 10的卡在50MHz时就有概率出现CRC错误48MHz反而稳定。CubeMX的时钟树配置里SDMMC1的时钟来源于PLL1Q默认频率往往是120MHz左右需要你在CubeMX的Clock Configuration页面里手动调整分频系数让SDMMC1的时钟落在48MHz附近。操作路径是Clock Configuration页面 - 找到SDMMC1的时钟树分支 - 调整PLL1Q的分频器。比如PLL1Q输出120MHz那么SDMMC1的时钟分频就设置成2.5对应实际48MHz。注意CubeMX里的分频值是可以带小数的因为SDMMC外设内部还有一个分频器可以和总线分频协同工作。配置完成后编译生成的代码里会有一个类似SDMMC_ClkCard之类的计算函数它会根据频率配置自动计算分频参数。不要忽视这一步我之前图省事直接用了CubeMX的默认时钟配置结果SDMMC1的时钟是60MHz读写时偶发卡死排查了整整一天才发现是超频导致的。2.3 MDMA配置绕过CPU才是H7的精髓STM32H743的MDMA算是一个特色外设它可以实现内存到内存、内存到外设的高效数据搬运关键是搬运过程不占用CPU核心。对于SD卡这种需要大量数据搬移的场景如果不用MDMA直接用CPU读取SDMMC的数据FIFOCPU占用率会飙升到难以接受而且SDMMC的FIFO是32字节深度的数据量大时CPU根本忙不过来。CubeMX里配置MDMA需要在MDMA页面添加一个通道请求源选择SDMMC1_RX或SDMMC1_TX方向根据数据流选择外设到内存读或内存到外设写。这里有个细节容易踩坑MDMA的通道选择不是随便选的H743的MDMA请求映射是固定的SDMMC1_RX对应MDMA通道0SDMMC1_TX对应MDMA通道1。配置MDMA的Buffer Size时要和SDMMC的数据长度一致。在实际使用中FATFS读写的扇区大小通常是512字节也就是一次读取的块大小是512字节那么MDMA的Buffer Size就设置成512Block Size设置为4字节对应32位总线宽度Block Count设置成128512/4128。这个计算逻辑不复杂但很多人容易把Block Size和Block Count搞反。优先级方面MDMA通道的优先级建议设置为Very High。为什么因为SDMMC在接收数据时如果FIFO满了而MDMA没有及时搬运SDMMC就会触发OVERRUN错误。优先级设低了一旦遇到中断风暴或者总线竞争数据就容易丢。2.4 FATFS的选择这个真不是选个盘符那么随意CubeMX的Middleware列表里可以勾选FATFS但注意它有两种模式一种是只挂载读写的通用模式一种是带编码转换的LFN长文件名模式。如果只是做数据记录文件名用8.3格式就够了LFN模式反而会增加代码量和处理复杂度。但如果你要支持中文文件名或者长文件名就必须开启LFN并且需要额外移植cc936.c或cc932.c这类编码表文件。CubeMX默认生成的FATFS配置会在user_diskio.c文件里实现底层接口包括disk_status、disk_initialize、disk_read、disk_write、disk_ioctl等函数。这些函数的实现基于HAL库的SDMMC驱动大部分情况是开箱即用的。但有一个坑disk_ioctl函数里有几个case比如GET_SECTOR_COUNT和GET_BLOCK_SIZE。H7的HAL库在HAL_SD_GetCardInfo函数中已经提供了卡信息但CubeMX生成的代码里GET_SECTOR_COUNT的返回值需要手动改成pDrive-sector_count之类的内容默认模板可能给的是固定的512字节或者干脆是0。此外还有个细节H743的SDMMC底层驱动里HAL_SD_ReadBlocks和HAL_SD_WriteBlocks在默认情况下是阻塞模式的即使你在CubeMX里开了MDMA生成的代码大概率还是调用阻塞模式的函数。这时候就需要手动修改user_diskio.c里的读写函数把阻塞调用替换成HAL_SD_ReadBlocks_DMA或HAL_SD_WriteBlocks_DMA并且要配合信号量或者事件标志来确保DMA传输完成。这个过程涉及同步机制处理不好就会出现读写返回成功但数据没写完的问题。3. 那些官方文档没细说的核心机制3.1 SDMMC的数据通路从SD卡到内存中间经过了多少个环节要理解后面的坑得先弄明白H743上SD卡数据是怎么流动的。以读SD卡为例数据从SD卡的存储单元出发通过SDMMC总线的数据线进入SDMMC外设的接收FIFOFIFO里的数据再通过总线传输到内存。但这里有一个关键点SDMMC外设本身有两个DMA接口一个是传统的DMA1/DMA2另一个是MDMA。CubeMX默认生成的代码里用的是DMA请求映射到MDMA但如果你在MDMA配置里没有正确关联到SDMMC外设实际搬数据的时候就会出现“第一个扇区读对了第二个扇区数据错位”的诡异现象。H7的SDMMC在DMA传输模式下支持缓冲区的自动换行操作也就是说你可以配置MDMA在缓冲区到达末尾后自动回卷到起始地址这样SDMMC就可以持续不断地向内存中写入数据不需要CPU干预缓冲区切换。这项功能对于连续多扇区读写非常有意义但前提是MDMA的配置要精准匹配SDMMC的FIFO深度。SDMMC1的FIFO深度是32字也就是128字节但MDMA单次传输的最大burst长度可以配置为1、2、4、8、16、32、64、128等。这里最容易出的问题就是MDMA的burst长度配置得过大比如配置成64或128而SDMMC每产生一次DMA请求只填充少量数据到FIFO结果MDMA一搬就是一大块数据就对不齐了。我在这次调试中把MDMA的burst长度设为4对应一次搬运4个32位字就很稳定。3.2 为什么H7上SD卡中断必须配优先级不配就死给你看FATFS在读写SD卡时底层disk_read函数是同步等待的但MDMA传输是异步的。这就需要一个机制来通知CPU“数据搬运完成了”。CubeMX生成的代码里SDMMC的中断处理函数SDMMC1_IRQHandler会调用HAL库的回调函数其中HAL_SD_RxCpltCallback就是数据接收完成回调。问题在于这个回调函数的执行上下文是中断如果你的NVIC里SDMMC中断优先级配得比MDMA低或者和某些系统节拍中断优先级冲突就可能出现回调一直不触发的情况。更常见的麻烦是FATFS的f_read函数在等待数据时如果底层用信号量做同步信号量等待的超时时间设置短了传输稍微慢一点就可能出现“读超时”。一种稳妥的方案是把SDMMC中断优先级设置为最低数值最大同时确保任何时刻只有一个SDMMC传输在进行。实测在IAR环境里把SDMMC中断优先级设为50-15范围MDMA完成中断如果有设为7这样既不干扰系统节拍也不会被其他外设中断打断。另外官方文档里Cortex-M7内核的中断有一个坑中断里不要调用任何阻塞函数比如HAL_SD_ReadBlocks的同步版本。一旦你在中断回调里调了阻塞函数轻则死等重则HardFault。CubeMX生成的模板代码默认没这个问题但如果你自己在回调里添加了处理逻辑就要小心。3.3 FATFS移植时容易忽略的“磁盘状态机”FATFS本身是一个文件系统层它对底层磁盘的接口抽象很好但底层接口的实现质量高低直接决定文件系统是否稳定。我这次在H743上遇到的最诡异的问题是这样的系统启动后第一次f_mount挂载SD卡返回成功但紧接着f_open创建文件就失败错误码是FR_NOT_READY。检查了卡、检查了接线最后发现是disk_status函数返回的状态一直不对。FATFS在操作前会调用disk_status查询磁盘状态如果返回值里有STA_NOINIT标志FATFS会认为磁盘没有初始化好拒绝后续操作。在CubeMX生成的disk_status函数里默认调用HAL_SD_GetCardStatus来获取状态但这个函数在H743上有一个特性如果SDMMC外设没有初始化或者卡没有正确识别它会一直返回超时。问题出在disk_initialize函数里如果初始化失败但你不去处理状态标志FATFS就会陷入“初始化失败-状态未就绪-拒绝操作”的死循环。我在代码里加了一个全局变量SD_CardInit_Success在disk_initialize里判断HAL_SD_Init和后续的卡初始化步骤是否完整走完只有当所有步骤都成功后才把状态清除。这样FATFS就不会因为状态标志混乱而拒绝操作。这个方法虽然土但放在这批坑里反而是最实用的一条经验。4. 实操从CubeMX工程到跑通读写全流程4.1 完整配置清单和生成代码前的确认把上面的要点串起来我现在写一份可以直接照着配的参数清单。打开CubeMX建立一个新的STM32H743VIT6工程引脚配置PC12(CLK)、PD2(CMD)、PC8~PC11(DATA0-DATA3)、PC6(CD)全部设为SDMMC1功能。PC12、PD2和PC8~PC11在CubeMX的芯片视图里直接点击从下拉列表里选SDMMC1即可CD引脚在SDMMC1的Card Detect选项里使能后手动指定。时钟配置外部HSE 25MHzSYSCLK跑到480MHzPLL1Q配置为120MHzSDMMC1的时钟分频设为2.5。确认Clock Configuration页面里SDMMC1旁边的数字显示为48MHz。SDMMC参数勾选SD卡支持位宽选4 bits时钟分频选择Bypass对应50MHz以下传输模式配置为DMA或者由代码动态切换。我这边最终选择使用DMA模式。中断配置SDMMC1全局中断使能优先级设为5。同时使能MDMA的全局中断不用也没关系但建议留着备用方便调试。FATFS配置勾选FATFS模式选为GenericTCHAR类型设置为charLFN使能选择Disabled如果你用不到长文件名最大扇区保持512。MDMA配置添加两个通道RX请求源选SDMMC1_RXTX请求源选SDMMC1_TXBuffer Size设置为512Block Size设为4字节Block Count设为128优先级设为Very High。配置完这些点击生成代码。工程生成后打开user_diskio.c先把GET_BLOCK_SIZE的返回值改成pdrv-block_size实际上这个值在disk_initialize里从卡信息中获取默认是512。然后把disk_read和disk_write函数体里的HAL_SD_ReadBlocks和HAL_SD_WriteBlocks替换成DMA版本并做好同步等待。4.2 底层读写函数改写的关键代码解析用DMA版本替换阻塞版本的核心代码如下这是工程里最关键的一段改造void disk_read_sync(void) { while (SD_read_semaphore 0) { // 等待DMA传输完成信号量 } SD_read_semaphore 0; }在disk_read里if (HAL_SD_ReadBlocks_DMA(hsd1, buff, sector, count) HAL_OK) { while (SD_read_semaphore 0) { // 等待 } SD_read_semaphore 0; }然后在HAL_SD_RxCpltCallback回调里void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd) { if (hsd-Instance SDMMC1) { SD_read_semaphore 1; } }这里的SD_read_semaphore只是在裸机环境下用一个volatile变量模拟信号量。如果在FreeRTOS环境里建议换成二值信号量或者事件标志组效果更好。另外注意HAL_SD_ReadBlocks_DMA函数的第三个参数是扇区地址不是字节地址FATFS传入的sector参数本身已经是扇区号所以直接透传即可。disk_write的改写逻辑和读一样只是把HAL_SD_ReadBlocks_DMA换成HAL_SD_WriteBlocks_DMA回调换成HAL_SD_TxCpltCallback。4.3 实际读写测试数据到底能不能落盘配置改写完成后我写了一个测试程序逻辑很简单FRESULT res; FATFS fs; FIL fil; UINT bytes_written, bytes_read; uint8_t write_buf[512]; uint8_t read_buf[512]; res f_mount(fs, , 1); // 挂载SD卡 res f_open(fil, test.bin, FA_CREATE_ALWAYS | FA_WRITE); for (int i 0; i 100; i) { memset(write_buf, i, 512); f_write(fil, write_buf, 512, bytes_written); } f_close(fil); res f_open(fil, test.bin, FA_OPEN_EXISTING | FA_READ); f_lseek(fil, 0); for (int i 0; i 100; i) { f_read(fil, read_buf, 512, bytes_read); if (read_buf[0] ! i) { // 数据校验失败 } } f_close(fil);用这个程序跑了两轮测试。第一轮写入100个扇区50KB校验全部通过。写入速度大约340KB/s读取速度约450KB/s。第二轮改成写入1024个扇区512KB校验同样全部通过。到这里MDMAFATFS这套方案就算是跑通了。从性能角度讲用MDMA相比直接CPU轮询读数据CPU占用率下降非常明显。在跑写512KB数据的测试中用逻辑分析仪抓SDMMC的数据线信号MDMA模式下CPU几乎只负责文件系统层的计算数据搬运完全交给了DMA通道。4.4 实测中遇到的性能瓶颈不仅靠DMA就万事大吉上面说了这个方案跑通后的效果但你别以为这样就结束了。我在性能调优阶段还遇到了一个“看似正常但实际很慢”的问题初始化挂载耗时特别长每次上电要卡2秒多才能进入文件操作。排查后发现是FATFS的挂载过程中内部会先读取SD卡的分区表。如果SD卡的MBR和引导扇区有大量填充数据读取次数会非常多。后来我在disk_ioctl的GET_SECTOR_COUNT实现对大数据块处理做了优化同时在挂载前主动调用一次disk_initialize并先读取几遍SD卡状态以唤醒卡片。这个做法减少了后续FATFS初始化时的重复握手时间。执行完这些小改动后从上电到成功f_open的耗时从2秒多降到了600毫秒左右体感明显好很多。5. 常见问题速查表与避坑清单5.1 我踩过的7个典型问题现象根因解决方案FATFS返回FR_NOT_READYdisk_status里状态标志未清除在disk_initialize末尾清除STA_NOINIT读写偶尔CRC错误SDMMC时钟超频或信号质量差将SDMMC时钟降到48MHz以下检查上拉电阻建议10k第一个扇区正常后面数据错位MDMA burst长度配置过大将MDMA burst长度设为4block size设为4字节DMA传输后数据未真正落盘写DMA完成回调未正确等待使用信号量同步写完成或调用HAL_SD_GetCardState确认f_mount卡死SD卡初始化握手超时在初始化前增加几次SD卡供电唤醒循环热插拔读卡失败Card Detect引脚电平不稳确认CD引脚与卡座硬件连接使能内部上拉代码加消抖逻辑写速度低于预期FATFS每次写一个扇区512字节效率低连续写大块数据或调整FATFS的簇大小和缓冲区大小5.2 一个被很多人忽略的写入坑关闭写保护检测CubeMX生成的FATFS代码里默认会调用disk_ioctl的GET_WRITE_PROTECT和GET_DRIVE_NUM等case。如果GET_WRITE_PROTECT返回了错误值FATFS会认为磁盘写保护拒绝写入。我在一次调试中遇到的现象是读取完全正常一写入就返回FR_WRITE_PROTECTED。最后发现是因为我在SD卡座的写保护检测引脚通常是卡座的WP开关信号接了上拉电阻但当卡插入时这个引脚被拉低而代码里的GET_WRITE_PROTECTcase直接返回了0表示未写保护导致逻辑反转。如果你的卡座上没有连接写保护检测引脚妥善的做法是直接在disk_ioctl的这个case里返回0并且不要让它走HAL库默认的检测逻辑因为这个引脚是完全悬空的话最终返回的电平是随机的很容易触发写保护误判。5.3 优先级、中断与缓存对齐H7特有的三座大山H7是Cortex-M7内核带数据缓存D-Cache这就牵扯到一个F4平台从未遇到过的内容如果MDMA要搬运的内存区域被D-Cache缓存了而且缓存里的数据和实际内存不一致那么MDMA搬运的数据大概率是错的。我在工程里是这么处理的给SD卡的读写缓冲区分配在特定的内存段并保证地址32字节对齐MDMA支持的最低对齐粒度然后在每次DMA传输前执行SCB_CleanDCache写操作前和SCB_InvalidateDCache读操作后。CubeMX生成的链接脚本默认把所有变量放在DTCM RAM里这没问题但如果你把缓冲区放到了AXI SRAM或者SRAM1/SRAM2就需要仔细核对cache一致性。这个问题的症状也很典型第一次读写数据正常第二次开始出现数据偶尔不对而且只有开优化后出现。基本就是D-Cache的一致性问题。如果你用的是FreeRTOS还要注意任务栈的地址也要对齐否则DMA写入时可能会跨越cache line边界造成边界数据错乱。5.4 CubeMX自定义引脚冲突排查要点在配置阶段如果你发现CubeMX里SDMMC1引脚和别的外设打架不要试图通过调整“Alternate function”来迂回解决。H743的引脚复用是高度固定的SDMMC1的引脚只能复用为SDMMC1功能。矛盾出现时要么调整其他外设的引脚要么换个SDMMC控制器如果芯片支持SDMMC2。我在测试中还试过SDMMC2它在H743上映射到了PD6(CLK)、PD7(CMD)、PD3~PD5(数据线)。如果你的设计里SDMMC1引脚被以太网或者FMC占用了可以考虑SDMMC2功能和SDMMC1完全一样。但注意SDMMC2在部分型号上可能不支持CAD卡检测特性这一点要细细看芯片参考手册。6. 最终测试结果和后续扩展想法经过这轮折腾我最终平台上的SD卡读写方案是这样定型的CubeMX配置SDMMC1 MDMARX/TX FATFS底层用户代码里加了一个全局信号量做DMA同步缓冲区放在SRAM3并做了32字节对齐每次DMA操作前后处理D-Cache一致性。实测Class 10 SDHC卡持续读速约480KB/S持续写速约360KB/SCPU占用率在读写期间几乎可以忽略系统其余任务运行完全不受影响。对于大多数数据记录类项目这个速度足够用。后面我计划在这个基础上扩展两个方向一是做SD卡热插拔监控通过CD引脚的外部中断实时监测卡片状态在拔卡前调用f_mount(NULL)释放文件系统二是把FATFS的缓冲策略改成FF_USE_FASTSEEK配合更大的簇大小在连续数据采集场景下把写速度再往上拉一档。跑数据记录类的项目SD卡这套东西稳定比速度重要得多。H743的MDMA功能如果配得好能做到数据搬移零CPU参与但一定要记得MDMA的每一个配置项都有它存在的特定意义改任何一个参数前先问自己一个问题这个参数的默认值背后的硬件逻辑我搞明白了没有这个习惯能帮你避开至少九成藏在官方文档里的坑。实测下来最顺手的组合分享一个CubeMX配完SDMMC和MDMA后FATFS底层不要急着用CubeMX生成的默认文件对比一下user_diskio.c里disk_ioctl的各个case把没用的检测功能全部改成最直接的返回值。这样一来FATFS的访问路径最短排查问题时也能少看几个分支。如果你正准备在H743上跑SD卡读写不妨拿着这份checklist逐项过一遍很多烦人的问题其实在配置阶段就已经注定了。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。