ARTICLE DETAIL

资讯详情

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

STM32H743 FreeRTOS下FATFS+MDMA读写SD卡避坑指南

STM32H743 FreeRTOS下FATFS+MDMA读写SD卡避坑指南 如果你和我一样在STM32H743上跑FreeRTOS同时想让FATFS通过SDMMC1读写SD卡那多半会在CubeMX的DMA Settings里看到MDMA那个选项。我当时是抱着试试高级功能的心态点下去的结果给自己挖了一串坑先是读出来的文件数据每隔一段变成0xFF接着两个任务同时碰FATFS时把文件系统搞挂最后连MDMA中断回调里调个信号量都能让系统崩掉。这篇文章把这几个雷完整记录下来包括每一步的排查思路和最终能直接用的配置给想在FreeRTOS下用MDMA做SD卡底层搬运的朋友做参考。1. 项目背景为什么我要在FreeRTOS里给FATFS配MDMA1.1 需求场景和数据丢包先交代硬件环境主控是STM32H743VIT6板载microSD卡槽SDMMC1走4-bit模式SD卡是SanDisk Class10 32GB。软件侧跑FreeRTOS任务划分大概是sensor_task每10ms采集一次传感器数据通过队列发给存储任务storage_task负责把数据组包、写FATFS、刷SD卡display_task跑UI显示基本不碰SD卡。最初我图省事直接用CubeMX默认生成的sd_diskio.c底层走的是HAL_MMC_ReadBlocks/HAL_MMC_WriteBlocks阻塞式读写。裸机测试时没问题但上了FreeRTOS之后问题立刻暴露SD卡写入一个512字节扇区如果卡忙或者FATFS在更新FAT表一次f_write能阻塞二十多毫秒。storage_task是独立任务理论上阻塞不会卡死其他任务但传感器数据是实时产生的队列一旦写满sensor_task就只能丢数据。跑业务时眼看着日志文件里出现连续性断层心里非常难受。1.2 从DMA到MDMACubeMX里那个不起眼的选项想改成DMA方式时打开CubeMX的SDMMC1配置页在DMA Settings标签下添加SDMMC1_RX和SDMMC1_TX请求往下看会发现请求类型除了常规DMA之外还有一个MDMA选项。一开始我也犹豫MDMA配置比普通DMA复杂HAL库里对应的初始化结构也不一样真有必要吗答案是在H743上有必要。1.3 为什么选MDMA而不是DMA1/DMA2STM32H743和F103、F407最大的区别之一是它引入了复杂的存储域划分。Cortex-M7内核既有TCM总线DTCM/ITCM又有AXI总线片内SRAM被拆成D1域的AXI SRAM、D2域的SRAM1/2/3、D3域的SRAM4等好几块。而DMA1/DMA2是挂在D2域总线上的外设它能访问的内存范围天然受限对比项DMA1/DMA2MDMA总线位置D2域AHB互联Cortex-M7系统总线内核级可访问存储以SRAM1/2/3等D2域为主对DTCM访问受限几乎全部存储空间包括DTCM/ITCM/AXI SRAM/SRAM4是否经过D-Cache不经过不经过通道数每DMA控制器8个流8个通道链表/描述符部分支持双缓冲原生支持Linked-list配置复杂度低较高在FreeRTOS工程里任务栈和用户buffer很可能被分配到DTCM或者AXI SRAM。我之前在别的H7项目里把DMA缓冲区放在DTCM结果DMA传输完成后缓冲区全是0没有任何错误提示排查了大半天。MDMA没有这个限制它作为内核级总线主控可以访问几乎全部存储器映射空间和FATFS这种“需要频繁搬运用户buffer”的场景配合起来非常舒服。当然选择MDMA的代价也很明显Cache一致性必须自己维护MDMA中断和FreeRTOS的优先级关系要仔细配置sd_diskio.c里的阻塞函数也要手动改成DMA版本。后面讲的三个雷基本都是这些代价的具体体现。1.4 我最终采用的架构改完之后的软件链路大概是这样storage_task调用FATFS API →ff.c处理文件系统逻辑 →sd_diskio.c的SD_read/SD_write→HAL_MMC_ReadBlocks_DMA/HAL_MMC_WriteBlocks_DMA→HAL_MDMA_Start_IT启动MDMA搬移 → SDMMC1外设控制器访问SD卡。应用层完全感知不到DMA的存在但底层已经从阻塞模式变成了中断驱动模式storage_task在发起传输后可以睡眠等信号量把CPU让给其他任务。2. CubeMX配置全景从SDMMC时钟到MDMA请求映射的关键选项这一章的配置是基于CubeMX 6.9、STM32CubeH7 HAL库1.11.1版本做的其他版本界面会略有差异但思路一致。2.1 时钟树SDMMC1时钟源选PLL1Q分频别乱填工程使用板载25MHz HSESYSCLK配到480MHz。在Clock Configuration里找到SDMMC1的时钟源我选的是PLL1Q输出48MHz。这里有个容易误解的地方SDMMC外设界面里的Clock Divider并不是随便填的。H7的SDMMC_CK计算公式是SDMMC_CK SDMMCCLK / (CLKDIV 2)如果SDMMCCLK是48MHzClock Divider填2那实际SDMMC_CK就是48 / (22) 12MHz这个速度下Class10卡能工作但远没到极限。如果希望跑到接近24MHz甚至更高需要把SDMMC时钟源配到更高的PLL输出比如PLL2P。我项目中为了稳妥SDMMC时钟源用了48MHzClock Divider填2实测读写速度也能接受具体数据见第六章。还要注意一个坑SD卡初始化阶段要求时钟低于400kHz。如果你在CubeMX里把Clock Divider填得很小HAL_MMC_Init会把分频值直接写入SDMMC_CLKCR某些卡会CMD0超时。市面上不少板子能跑通是因为卡的兼容性好遇到体质差的卡就翻车。稳妥做法是CubeMX里暂时填一个较大的分频值比如11848MHz源时得到400kHz确认卡初始化成功后再在代码里把CLKCR改成高速分频。2.2 SDMMC1外设配置4-bit模式与DMA请求添加在Device Configuration Tool里左侧Categories选择Multimedia打开SDMMC1Mode选SD 4-bit Wide bus切换到DMA Settings标签点Add下拉添加SDMMC1_RX和SDMMC1_TX两个请求把请求类型从普通DMA改成MDMACubeMX会自动为RX和TX分配MDMA通道通常一个用CH0一个用CH1。这里需要检查两件事生成的MDMA初始化结构里Init.Request参数必须分别对应SDMMC1_RX和SDMMC1_TX如果两个通道的Request配反了搬运方向就是错的读写数据会乱MDMA的BufferSize字段是16位的如果单次搬运长度超过65535字节需要拆块或者改用链表描述符。FATFS底层虽然一般按扇区读写但大块连续写时FATFS内部也会合并成较大的请求要留意。生成代码后在stm32h7xx_hal_msp.c里检查MDMA的时钟使能。MDMA挂在AHB3上CubeMX会调用__HAL_RCC_MDMA_CLK_ENABLE()如果发现MDMA不工作优先排查这个时钟有没有被正确开启。2.3 NVIC中断优先级MDMA中断必须让FreeRTOS能管MDMA在H743上只有一个中断向量MDMA_IRQn不是每个通道一个。在NVIC设置里打开MDMA_IRQn后优先级要特别小心。FreeRTOS的configMAX_SYSCALL_INTERRUPT_PRIORITY默认值是5意思是优先级数值大于等于5的中断才能安全调用FromISR结尾的FreeRTOS API。Cortex-M的优先级是数字越小优先级越高所以MDMA中断优先级如果填0或1它比FreeRTOS可管理的优先级还要高系统会拒绝在中断里做任务切换直接触发configASSERT。我的配置是Preemption Priority填5Sub Priority填0。这样MDMA中断可以被FreeRTOS管理在中断回调里调用信号量give是完全安全的。2.4 FATFS与FreeRTOS的集成参数设置在Middleware and Software Packs里勾选FATFS模式选SD Card。然后在FATFS配置页面里把Use Reentrancy打开对应ffconf.h中的FF_FS_REENTRANT变成1这是第四章会讲的关键开关。FF_FS_TIMEOUT我填1000表示卷锁等待超时1秒。FreeRTOS配置使用CMSIS_RTOS V2接口。我创建了storage_task和sensor_task堆大小给了256KB。H743片内RAM很大FreeRTOS堆给到256KB完全合理后续任务栈不愁分配。2.5 生成后必须改的sd_diskio.c把阻塞读写改成DMA读写这是最容易被忽略的一步。CubeMX生成的sd_diskio.c即使在你配好MDMA之后SD_read和SD_write默认还是调用阻塞版本的HAL_MMC_ReadBlocks/HAL_MMC_WriteBlocksDMA根本不会被用上。必须手动把这两个函数改成DMA版本。改造思路很简单创建两个信号量一个给读完成用一个给写完成用。在中断回调里释放信号量在SD_read/SD_write里等待信号量。/* sd_diskio.c 顶部 */ #include cmsis_os.h static osSemaphoreId_t sd_rx_sem; static osSemaphoreId_t sd_tx_sem;初始化时创建信号量初始计数为0void SD_InitSemaphores(void) { sd_rx_sem osSemaphoreNew(1, 0, NULL); sd_tx_sem osSemaphoreNew(1, 0, NULL); }中断回调函数void HAL_MMC_RxCpltCallback(MMC_HandleTypeDef *hmmc) { osSemaphoreRelease(sd_rx_sem); } void HAL_MMC_TxCpltCallback(MMC_HandleTypeDef *hmmc) { osSemaphoreRelease(sd_tx_sem); } void HAL_MMC_ErrorCallback(MMC_HandleTypeDef *hmmc) { osSemaphoreRelease(sd_rx_sem); osSemaphoreRelease(sd_tx_sem); }SD_read改成static DRESULT SD_read(BYTE lun, BYTE *buff, LBA_t sector, UINT count) { uint32_t timeout 3000; if (HAL_MMC_ReadBlocks_DMA(hmmc, (uint32_t *)buff, sector, count) ! HAL_OK) { return RES_ERROR; } if (osSemaphoreAcquire(sd_rx_sem, timeout) ! osOK) { return RES_TIMEOUT; } return RES_OK; }SD_write改成static DRESULT SD_write(BYTE lun, const BYTE *buff, LBA_t sector, UINT count) { uint32_t timeout 3000; if (HAL_MMC_WriteBlocks_DMA(hmmc, (uint32_t *)buff, sector, count) ! HAL_OK) { return RES_ERROR; } if (osSemaphoreAcquire(sd_tx_sem, timeout) ! osOK) { return RES_TIMEOUT; } return RES_OK; }注意HAL_MMC_ReadBlocks_DMA和HAL_MMC_WriteBlocks_DMA的缓冲区参数是uint32_t *在FATFS里传进来的是BYTE *直接强转就行但要保证缓冲区地址至少4字节对齐。如果应用层传了非对齐的bufferFATFS内部通常会先经过自己的对齐缓冲区但保险起见还是建议用户层把文件读写buffer对齐到4字节以上。改完这层MDMA才开始真正参与FATFS的数据搬运。而一旦MDMA参与进来第一个大雷就来了。3. 第一个雷MDMA搬运的数据是脏的——D-Cache一致性问题全解析3.1 现象读文件读出一段段0xFF改完DMA读写的当天我做了个简单测试读取SD卡上的一个1MB文本文件校验内容。结果文件前几千字节完全正常往后每隔一段就出现大片0xFF或者旧数据更诡异的是单步调试时数据是对的全速跑就错。这个现象让我一度怀疑是SD卡坏了换了三张卡问题依旧。3.2 根因Cache视图和内存视图不一致问题出在Cortex-M7的D-Cache上。H743的D-Cache一旦使能CPU读写内存就不再直接操作物理内存而是先经过Cache。在write-back模式下CPU写入数据时可能只写进Cache Line并没有立刻落回物理内存CPU读数据时也可能直接命中Cache里的旧数据。而MDMA是总线主控它读写的是物理内存完全绕过了Cache。于是系统里出现了两个主人看到的物理内存视图不一样CPU写过的数据可能还躺在Cache里没来得及刷回内存MDMA从外设搬进内存的数据CPU如果Cache命中读到的是旧缓存行。我在前面的SD_read函数里只等信号量就返回了完全没有管Cache所以CPU从buffer里读到的内容是“旧内存 新数据”混在一起的样子。每隔一段出现0xFF是因为一个Cache Line是32字节32字节边界正好和文件内容边界错开导致部分行是旧的。3.3 一个直观类比把Cache想象成你办公桌上的待办便签物理内存是公司的共享文件柜。CPU这个人办事很快习惯先把文件放在桌上写Cache等着有空再归档到文件柜。MDMA是另一个同事他不管你的桌子直接去文件柜拿文件。如果你刚在桌上改完数据还没归档MDMA去文件柜拿到的就是旧版文件反过来MDMA把新文件放进文件柜你桌上一看还有旧便签的记录就以为文件柜里还是旧内容。3.4 修复Clean和Invalidate的时机明白了原理修复方式就清晰了MDMA写内存SD卡读传输完成后对缓冲区地址执行Invalidate。把Cache Line无效化CPU下次访问时强制从物理内存重新加载内存写MDMASD卡写启动MDMA传输前对缓冲区地址执行Clean把Cache里的脏数据写回物理内存确保MDMA搬走的是最新内容。注意H7的D-Cache Line大小是32字节SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr这两个函数的地址和长度都要按32字节对齐否则操作范围会错位。我封装了两个工具函数#define DCACHE_LINE_SIZE 32U static void cache_invalidate_range(uint32_t addr, uint32_t size) { uint32_t start addr ~(DCACHE_LINE_SIZE - 1U); uint32_t end (addr size DCACHE_LINE_SIZE - 1U) ~(DCACHE_LINE_SIZE - 1U); SCB_InvalidateDCache_by_Addr((uint32_t *)start, end - start); } static void cache_clean_range(uint32_t addr, uint32_t size) { uint32_t start addr ~(DCACHE_LINE_SIZE - 1U); uint32_t end (addr size DCACHE_LINE_SIZE - 1U) ~(DCACHE_LINE_SIZE - 1U); SCB_CleanDCache_by_Addr((uint32_t *)start, end - start); }然后在SD_read和SD_write里对应加上Cache维护static DRESULT SD_read(BYTE lun, BYTE *buff, LBA_t sector, UINT count) { uint32_t timeout 3000; if (HAL_MMC_ReadBlocks_DMA(hmmc, (uint32_t *)buff, sector, count) ! HAL_OK) { return RES_ERROR; } if (osSemaphoreAcquire(sd_rx_sem, timeout) ! osOK) { return RES_TIMEOUT; } cache_invalidate_range((uint32_t)buff, count * 512); return RES_OK; }static DRESULT SD_write(BYTE lun, const BYTE *buff, LBA_t sector, UINT count) { uint32_t timeout 3000; cache_clean_range((uint32_t)buff, count * 512); if (HAL_MMC_WriteBlocks_DMA(hmmc, (uint32_t *)buff, sector, count) ! HAL_OK) { return RES_ERROR; } if (osSemaphoreAcquire(sd_tx_sem, timeout) ! osOK) { return RES_TIMEOUT; } return RES_OK; }改完之后再测文件内容完全正常。3.5 另一个思路通过MPU把缓冲区设为non-cacheable如果不想每次搬运都手动维护Cache还有一种办法在MPU配置里把FATFS缓冲区所在的某块内存区域设置为Normal memory, Non-cacheable。这样一来CPU读写这块内存时直接访问物理内存Cache不会介入天然一致。代价是该区域的CPU读写性能会下降但如果只是给SD卡缓冲用性能完全够。我没选这个方案是因为FreeRTOS堆可能从大块RAM里分配如果整块设为non-cacheable其他模块的Cache红利就没了。手动维护Cache其实也就两行代码性能更好。3.6 检查HAL库版本避免重复操作不同版本的STM32CubeH7 HAL库对Cache的处理不同。有些版本的HAL_MMC_RxCpltCallback里已经自带了SCB_InvalidateDCache_by_Addr如果你的HAL库里已经做了这件事自己再加一次也不会出错只是白白浪费时间。建议看一下stm32h7xx_hal_mmc.c源码确认HAL库是否已经做了Cache维护再决定要不要自己加。我用的1.11.1版本里MMC的DMA回调是没有Cache维护的所以只能自己来。4. 第二个雷FATFS在FreeRTOS里精神分裂——多任务的线程安全4.1 现象两个任务一碰SD卡文件系统就崩Cache问题解决后单任务写日志跑了一晚上正常。接着我加了一个config_task启动时从SD卡读配置文件。结果问题立刻出现f_open偶尔返回FR_DENIED两个任务同时打开文件时互相覆盖写入内容最严重的一次SD卡里的目录结构直接损坏插到PC上提示需要格式化。当时还以为是MDMA配置有问题因为两个任务都会触发SDMMC MDMA传输。排查了很久最后发现根本不是DMA的问题而是FATFS自己就不支持多任务并发访问。4.2 根因FF_FS_REENTRANT默认是0FATFS是个非常经典但相对原始的嵌入式文件系统它默认没有做任何线程保护。ffconf.h里的FF_FS_REENTRANT默认值是0意味着FATFS内部所有数据结构——FAT表项、目录项、文件分配信息——都没有锁保护。两个任务同时操作同一个卷时极可能出现这种场景任务A从FAT表里读取簇分配信息准备写文件任务B同时也在更新FAT表两个任务对同一块内存做读-改-写数据就乱了。如果只是FATFS内部buffer被踩还好严重时整个FAT表写坏SD卡里的文件系统就彻底损坏。4.3 为什么很多样例工程没这个问题因为大多数CubeMX生成的FATFS示例代码都只在主循环或者单任务里读写SD卡。只要同一时刻只有一个任务碰FATFS这个问题永远不会暴露。一旦任务数量超过一个而且恰好共享同一张SD卡竞争窗口就出现了。而且是概率性问题可能跑几小时没事也可能几分钟就炸。4.4 解决方案开启FATFS重入支持 实现syscall.cCubeMX的FATFS配置页面里有一个Use Reentrancy选项打开后ffconf.h会生成#define FF_FS_REENTRANT 1 #define FF_FS_TIMEOUT 1000 #define FF_SYNC_t osSemaphoreId_t但这只是启用了“需要同步”的开关真正干活的同步函数在syscall.c里。CubeMX会根据RTOS类型生成一个模板但有时模板是空的或者不完整需要自己补。我用CMSIS-RTOS V2实现如下#include cmsis_os.h #include ff.h int ff_cre_syncobj(BYTE vol, FF_SYNC_t *sobj) { *sobj osSemaphoreNew(1, 1, NULL); return (*sobj ! NULL) ? TRUE : FALSE; } int ff_del_syncobj(BYTE vol, FF_SYNC_t sobj) { osSemaphoreDelete(sobj); return TRUE; } int ff_req_grant(FF_SYNC_t sobj) { return (osSemaphoreAcquire(sobj, FF_FS_TIMEOUT) osOK) ? TRUE : FALSE; } void ff_rel_grant(FF_SYNC_t sobj) { osSemaphoreRelease(sobj); }这里提一个细节我故意用了信号量而不是互斥量。互斥量语义上更严格支持递归获取、优先级继承但CMSIS-RTOS v2的信号量完全满足FATFS的使用场景而且CubeMX生成的FF_SYNC_t类型和osSemaphoreId_t匹配最省事。如果你更严谨可以把FF_SYNC_t改成osMutexId_t然后在ff_cre_syncobj里用osMutexNew创建互斥量。4.5 注意FATFS的锁是卷级锁不是文件级锁开启重入支持后FATFS对并发访问的保护是“整个卷一把锁”。也就是说config_task在读配置文件时storage_task的日志写入也会被阻塞二者串行执行。这在大多数场景下是可以接受的因为FATFS本身的设计就不允许两个任务在同一卷上并行操作不同文件——它会直接阻塞而不是报错。如果业务确实需要多文件并发FATFS的架构先天不支持需要换LittleFS或者FileX这类支持多任务的现代文件系统。对我的项目来说卷级锁完全够用。4.6 挂载操作不要在多任务里重复执行还有一个常见问题多个任务各自调用f_mount挂载SD卡。挂载过程要扫描FAT表和目录如果两个任务同时挂载照样竞争。正确做法是系统初始化时由一个storage_init函数挂载一次用一个全局标志保护之后所有任务直接用挂载好的FF对象。5. 第三个雷MDMA完成中断与FreeRTOS调度打架5.1 现象信号量give导致断言失败任务傻等解决了Cache和线程安全后系统稳定了一阵子。但很快又冒出新问题运行一段时间后configASSERT突然触发或者某个任务的SD卡读操作一直等不到信号量直接超时整个存储功能瘫痪。5.2 原因一MDMA中断优先级超出FreeRTOS可管理范围前面在第二章里已经提到MDMA中断优先级要配到5但我一开始没注意CubeMX默认给MDMA中断分配的是优先级0。优先级0高于configMAX_SYSCALL_INTERRUPT_PRIORITY这意味着FreeRTOS在中断上下文里不能安全调用xSemaphoreGiveFromISR一旦调用就会触发configASSERT。所以第一件事就是把MDMA中断优先级改成5或更高数值更低实际优先级确认和FreeRTOS的配置匹配。5.3 原因二RX和TX共用一个信号量我最初偷懒只建了一个信号量RX和TX完成回调都give同一个。结果出现了一个隐蔽的竞态FATFS内部经常“先读后写”或“先写后读”比如写文件前需要读取FAT表。当读操作和写操作紧挨着发生时读完成给了一次信号量紧接着启动的写操作可能刚好acquire到了这个“别人的信号量”立刻返回成功但写数据其实还没搬完。这种问题极其恶心因为不是必现只有任务切换的时机恰好落在某个窗口才触发。修复方法很简单拆成两个独立信号量读等sd_rx_sem写等sd_tx_sem互不干扰。5.4 原因三任务栈深度不足表象是SD卡挂掉另一个我排查了很久的问题storage_task的栈只有512 words跑着跑着就死锁或者HardFault。一开始怀疑是MDMA中断优先级配错了后来用uxTaskGetStackHighWaterMark(storage_task_handle)一查发现栈高水位几乎到0。FATFS在长文件名解析、路径遍历、内部缓冲切换时的栈消耗很大再加上SD_read/SD_write的封装和信号量等待512 words根本不够。把storage_task的栈改成2048 words后再也没出现过类似的死锁。H743的RAM足够大任务栈给大一点不丢人重点是用HighWaterMark实测确认。5.5 封装成可等待的原子操作最终读写封装保持简单避免在diskio层再加多余的互斥锁。FATFS的Reentrant锁已经保护了整个卷diskio层再套锁反而可能引入死锁风险锁顺序问题。只依赖信号量做传输完成通知就够了。最终的SD_read基本就是static DRESULT SD_read(BYTE lun, BYTE *buff, LBA_t sector, UINT count) { uint32_t timeout pdMS_TO_TICKS(3000); if (HAL_MMC_ReadBlocks_DMA(hmmc, (uint32_t *)buff, sector, count) ! HAL_OK) { return RES_ERROR; } if (osSemaphoreAcquire(sd_rx_sem, timeout) ! osOK) { return RES_TIMEOUT; } cache_invalidate_range((uint32_t)buff, count * 512); return RES_OK; }5.6 超时参数别设太短SD卡在长时间空闲后第一次访问会比较慢尤其是从低功耗状态唤醒或者卡内部做磨损均衡时一个简单的读命令可能耗几百毫秒。如果你的读超时设置成500ms这类正常慢访问会被误判为超时然后返回RES_TIMEOUTFATFS就会报错。我最终把超时设置成3000ms既不会卡死任务太久又能容忍SD卡的正常慢速响应。6. 从HardFault到稳定运行排查链路与验证手段6.1 HardFault定位三板斧嵌入式开发里遇到HardFault是常态关键是快速定位。我总结了三板斧在HardFault_Handler里打断点停下来后查看SCB-CFSR、SCB-HFSR、SCB-BFAR、SCB-MMFAR这几个寄存器明确错误类型。如果是IMPRECISERR总线错误非精确大概率是DMA或MDMA访问了非法内存地址如果是STKOF栈溢出优先查任务栈深度。把FreeRTOS的configCHECK_FOR_STACK_OVERFLOW设为2实现vApplicationStackOverflowHook栈溢出时直接进钩子函数比在HardFault里分析快得多。如果MDMA自身报错查看MDMA-CESR寄存器里的错误标志尤其是Bus Error相关的位确认源地址或目的地址是否超出了MDMA可访问的范围。6.2 Cache问题的隔离实验第3章里的Cache问题我的排查链路其实是这样的换SD卡排除卡本身问题换读接口用阻塞模式读取同一文件一切正常说明SD卡和FATFS本身没问题回到DMA模式在全速运行前临时调用SCB_DisableDCache()关闭D-Cache发现问题消失确认和Cache一致性有关重新打开D-Cache在SD_read/SD_write里加上Clean/Invalidate问题消失。这个隔离实验成本很低但能精准锁定方向强烈建议遇到类似问题时做一次。6.3 长时间稳定性测试结果所有问题修复后我做了一轮完整的稳定性测试连续写日志24小时共写入约8GB数据文件切换约8000次断点重启后文件系统正常PC可正常读取实测Class10卡512字节块连续写约8-9MB/s4KB块顺序写约10-12MB/s读约13-15MB/s写一个4KB块时storage_task的阻塞等待时间从阻塞模式的2-15ms最差30ms降到几百微秒到几毫秒sensor_task没有丢包。6.4 最终可复用配置清单配置项推荐值说明SDMMC1模式SD 4-bit Wide bus速度快SDMMC1时钟源PLL1Q 48MHz根据板子调整Clock Divider2或更高保证初始化稳定卡初始化建议≤400kHzMDMA_RX通道MDMA_CH0CubeMX自动分配MDMA_TX通道MDMA_CH1CubeMX自动分配MDMA NVIC优先级Preemption5, Sub0与configMAX_SYSCALL_INTERRUPT_PRIORITY匹配FATFS FF_FS_REENTRANT1多任务操作FATFS必开FATFS FF_FS_TIMEOUT1000卷锁等待超时FATFS FF_SYNC_tosSemaphoreId_tCMSIS-RTOS v2信号量FreeRTOS heap256KBH743 RAM足够大storage_task栈1024-2048 words用HighWaterMark实测为准SD读写超时3000ms容忍SD卡慢速响应6.5 最后再分享一个小经验MDMA给FATFS搬数据并不是性能银弹但它确实解决了H743上DMA访问存储域受限的硬伤也大幅降低了SD卡IO对任务实时性的冲击。只是这条路需要把三件事都验证到位Cache一致性、FATFS线程安全、MDMA中断与FreeRTOS的协同。这三个坑每个都能让你排查好几天但想通原理之后每处修复其实也就几行代码的事。特别是Cache那个坑我后来在另一个SPI DMA项目里也遇到类似问题用同样的思路几分钟就定位了。希望这篇文章能帮你少走这段弯路。
返回列表