STM32H7上FreeRTOS与SDMMC冲突导致FatFs挂载失败的原因与解决
2026/8/30 7:20:29 网站建设 项目流程

STM32H7上同时跑FreeRTOS和SDMMC,FatFs在f_mount这一步直接给我甩了个失败返回值,这个坑我踩了不止一次。先亮个结论:大部分这类问题并不是FatFs本身导致的,而是RTOS环境改变了SDMMC外设的运行条件。这篇文章我会把从现象、原理、排错到最终配置的完整链路全部拆开,适合正在调STM32H7 + SDMMC + FreeRTOS + FatFs组合的嵌入式工程师参考,特别是那种“裸机能过,一加RTOS就挂”的诡异情况。

1. 同一个f_mount调用,裸机秒过、带上FreeRTOS就挂——先把问题边界划清楚

1.1 现象复现与错误码信息

我调试的板子是STM32H750,SD卡用的是4线SDMMC模式,CubeMX生成代码,底层HAL库,上面跑了FreeRTOS。单独一个裸机工程,同一个SD卡、同一套引脚配置,f_mount一次就过,读写文件也正常。一旦把SDMMC初始化和文件系统挂载这段逻辑放进FreeRTOS任务里,f_mount返回值经常不是FR_NOT_READY(3)就是FR_NO_FILESYSTEM(13)。偶尔能挂上,但复位后再挂又失败,完全看心情。

先说FatFs返回码的含义,这对排查方向很重要:

返回值十进值含义
FR_OK0成功
FR_NOT_READY3物理驱动器无法工作,底层驱动初始化失败或读取扇区失败
FR_NO_FILESYSTEM13卷上没有有效的FAT文件系统,可能是卡没格式化,也可能是底层读错了数据
FR_INT_ERR2底层驱动返回了预期外的扇区数或状态

我看到FR_NOT_READY,第一反应是底层SD卡驱动没有准备好。但问题是裸机明明能过,驱动本身应该是可用的。真正的原因集中在:RTOS的调度行为、中断优先级、内存访问权限、时基冲突,这几方面改变了SDMMC底层驱动的工作条件。

1.2 初步排除清单:不是所有卡和工程都有问题

在深入代码之前,我建议先快速排除几个最常见、代价最低的因素,避免在后面排查中把时间浪费在配置错误上。

第一,SD卡本身格式。FatFs默认配置下只支持FAT12/16/32,如果卡是exFAT,旧版本FatFs会返回FR_NO_FILESYSTEM。我的卡是FAT32格式,但也不排除有些卡出厂是exFAT。建议一开始就在Windows或Linux上格式化FAT32,排除这个变量。

第二,CubeMX生成的SDMMC引脚配置。这里最容易漏的是CMD、CK、D0-D3的上下拉电阻。SD卡总线在空闲状态下,CMD和D0-D3都需要被拉高,如果CubeMX生成的GPIO配置里没有正确设置上拉,卡可能偶尔能识别,偶尔不行。裸机能过说明大概率没问题,但如果你的板子上有外部上拉,这个因素可以放一放。

第三,SDMMC的时钟频率。初始化阶段SD卡时钟必须在400kHz以下(SD规范要求),传输阶段可以切到25MHz或50MHz。CubeMX会自动处理分频值,但如果你手动改过时钟树,初始化时钟可能被设置得太高,导致CMD0直接无响应。这个也要先确认一遍。

把以上三个变量排除之后,剩下的问题才是真正和FreeRTOS相关的,下面一层层分析。

2. FreeRTOS + SDMMC组合下的三大“隐形刺客”:时序、内存、中断优先级

2.1 SD卡初始化过程的硬性时序,为什么会被RTOS悄悄撕碎

很多人在调SD卡时并不关心SD卡协议本身的时序要求,HAL库封装得太好,导致我们以为只要调用HAL_SD_Init就万事大吉。实际上,SD卡上电后存在一个明确的初始化序列:上电至少等待2.5ms,让卡内部电源稳定;然后发送CMD0进入空闲态,接着发送CMD8确认电压范围,再发送ACMD41不断轮询直到卡退出上电流程(busy状态结束)。

这个过程有一个关键特性:ACMD41的轮询次数在极端情况下可能很多,尤其是首次上电时,卡内部如果正在做坏块管理或擦除,响应会明显变慢。裸机环境下,CPU从发CMD0到等ACMD41的过程中没有被其他代码抢占,几百毫秒内完成初始化很正常。但在FreeRTOS环境下,初始化代码运行在一个任务里,如果这个任务优先级不是最高,那么它随时可能被其他任务抢占。

一旦ACMD41的响应延迟刚好超过HAL库内部的超时时间(HAL_SD_Init内部用的是基于HAL_GetTick的超时循环),就会被判定为超时,卡初始化失败。更微妙的是,有些卡在ACMD41失败一次后,需要重新发送CMD0才能恢复状态机,而我们的代码并不会自动做这种重试,最后f_mount自然返回FR_NOT_READY。

这个问题的本质不是SDMMC本身的问题,而是RTOS的任务抢占影响了SD卡初始化函数的连续执行。解决办法并不是把所有任务都暂停(这会违背用RTOS的初衷),而是要在设计上保证:给SD卡初始化任务一个足够高的优先级,或者把整个初始化放在启动调度器之前完成,或者加入重试机制。

2.2 H7专坑:DMA看不见DTCM里的数据

如果你用CubeMX选择了SDMMC的DMA模式(很多工程为了性能都会选DMA),那么恭喜你,H7还有一个独特的深坑等着——DMA无法访问DTCM内存。

STM32H7的内存布局里,DTCM(0x20000000 - 0x2001FFFF)是直接挂在CPU内核上的紧耦合内存,访问延迟极低,因此一些启动文件、堆栈默认会被链接到DTCM区域。但问题是:片上外设的DMA控制器是访问不了DTCM的。如果你定义在C文件里的全局缓冲区被链接到了DTCM,SDMMC的IDMA在读取或写入时,根本拿不到数据。

具体表现就是:HAL_SD_ReadBlocks返回HAL_OK,但缓冲区的数据全是0x00或0xFF;或者HAL_SD_WriteBlocks返回HAL_OK,但卡上写的是乱码。FatFs去读MBR(主引导记录)时,读到全是空数据,识别不到有效的分区表,于是返回FR_NO_FILESYSTEM或FR_INT_ERR。

这个问题在裸机工程里可能不会暴露,因为裸机工程如果没开优化,变量可能被分配到SRAM或者AXI SRAM;但如果开了高优化等级,或者工程默认链接脚本把所有静态变量放DTCM,就会踩中。而FreeRTOS工程中,堆栈、任务控制块,甚至用户全局变量,都可能因为链接脚本配置而被塞进DTCM,所以带上RTOS反而更容易触发这个隐藏问题。

解决思路很简单:把SDMMC DMA所需的数据缓冲区明确放到DMA可访问的区域,比如AXI SRAM(0x24000000)、SRAM1/2/3(0x30000000)或者SRAM4(0x38000000)。最稳妥的做法是定义一个大数组并指定段属性。

2.3 中断优先级配置违背FreeRTOS规则,回调函数直接死锁

第三个隐形刺客是中断优先级,这个坑非常隐蔽,而且一旦踩中,排查成本极高。

FreeRTOS要求所有能从ISR中调用FreeRTOS API的中断,其优先级数值必须大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY。我用CubeMX生成FreeRTOS工程时,默认configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY是5,也就是说中断优先级数值不能小于5,否则不能在中断里调用FromISR结尾的函数。

SDMMC的DMA传输完成中断,或者SDMMC自身的全局中断,在HAL库的例程里往往被设置为最高优先级(优先级数值为0或1),因为在裸机环境下我们希望SD卡中断能及时响应。但放到FreeRTOS工程里,如果SDMMC中断优先级高于FreeRTOS可管理中断的优先级阈值,就会出问题。

最常见的情况是:当SDMMC中断触发时,FreeRTOS内核正在临界区或调度器被锁定的状态下,但此时中断又调用了FromISR函数,导致内核状态被破坏。表现可能是挂载偶尔成功、偶尔崩溃,也可能是f_mount在调用过程中直接进入HardFault,或者卡死在某个等待信号量的地方。

另外,还有一个H7上非常容易忽略的问题:如果使用了DMA模式,DMA的传输完成中断也需要设置合适的优先级。很多工程只改了SDMMC本身的中断优先级,忘了改DMA的中断优先级,同样会造成问题。

还有一个和FreeRTOS关系很大的坑:HAL库默认使用SysTick作为时间基准,但FreeRTOS也使用SysTick作为系统节拍。两个功能抢同一个中断源,RTO系统节拍和HAL的HAL_GetTick都会乱套。HAL_Delay和SDMMC的超时判断全依赖HAL_GetTick,一旦时基被FreeRTOS接管,HAL_GetTick可能长时间不更新或跳变,SD卡初始化函数的超时判断就会失效。CubeMX在生成FreeRTOS工程时,通常会自动把HAL时基切换到TIM6或其他定时器,但如果你自己手动搭工程,这一步经常被漏掉。

3. 完整排查链路:从CubeMX配置到diskio实现,一层一层剥开问题

3.1 第一步:在裸机工程里建立挂载基线

我自己排查这类问题,第一步永远是先建立一个裸机基线。不是因为裸机工程一定正确,而是为了分离变量:如果裸机下SD卡读写都正常,那么SD卡硬件、电路、格式、SDMMC基本配置都OK,问题就极大概率出在RTOS环境相关部分;如果裸机下都失败,那先别碰RTOS,把硬件和底层驱动调通再说。

我手头有一个最小裸机工程,CubeMX里SDMMC1配成SD 4-bit模式,时钟树确认SDMMC1时钟源为PLL1Q或PLL2P,频率不超过200MHz。生成代码后,只调用HAL_SD_Init、HAL_SD_ReadBlocks、HAL_SD_WriteBlocks,再加上FatFs。测试结果是:格式化、文件创建、读写全部正常。这一步确认了硬件和驱动基础没问题。

如果你发现裸机也失败,可以检查这几个点:

  • SDMMC1时钟源是否打开,时钟树里SDMMC1时钟显示是否正常。
  • SDMMC引脚复用是否选对,H7的SDMMC1引脚较多,复用到PF0、PF1、PD2、PC8-PC12这些,不同封装差异大。
  • 卡座上是否有CD(卡检测)引脚。如果板子没有接CD脚,在CubeMX里应该把SD卡检测配置为disabled,或者在代码里不检测CD状态。

3.2 第二步:核对时基与SysTick归属

当我确认裸机OK后,回到FreeRTOS工程,第一步就是检查HAL时基。打开stm32h7xx_hal_conf.h,看HAL_TICK_FREQ和时基来源。如果是CubeMX一键生成的FreeRTOS工程,它会在main.c里初始化TIM6作为HAL时基源。但如果你是自己移植的,很可能忘了这一步。

这一步排查最快:在FreeRTOS启动后(调度器跑起来后),打印HAL_GetTick(),看它是否正常递增。如果HAL_GetTick根本不走,或者走得很慢,那就说明SysTick被FreeRTOS占用,HAL时基没有切换。

解决办法也很直接,CubeMX里在SYS选项里面,把Timebase Source从SysTick改成TIM6,重新生成代码。这样HAL_GetTick由TIM6提供,FreeRTOS继续使用SysTick,两者互不干扰。SDMMC的HAL超时也就不再依赖被FreeRTOS劫持的SysTick了。

3.3 第三步:检查中断优先级分组与SDMMC回调路径

接下来是中断优先级。在FreeRTOS工程里,必须确保NVIC优先级分组为Group4(4位抢占优先级,0-15),FatFs和SDMMC相关的所有中断优先级数值都设为大于等于5。

我一般直接把SDMMC1全局中断和DMA中断都设为优先级5,这样既能满足FreeRTOS的可管理要求,又能保证中断响应速度不至于太低。其实对于SD卡这种外设,跑在50MHz时钟下,中断实时性要求并不苛刻,完全可以把优先级往低放。

代码层面,检查HAL_SD_MspInit里是否有这样的配置:

HAL_NVIC_SetPriority(SDMMC1_IRQn, 5, 0); HAL_NVIC_EnableIRQ(SDMMC1_IRQn); HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 5, 0); HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);

需要注意,H7上SDMMC的DMA可能走DMA2的Stream3或Stream6,具体取决于CubeMX自动生成的代码。关键是所有相关中断优先级都要大于等于configMAX_SYSCALL_INTERRUPT_PRIORITY的值。

另外,SDMMC的HAL回调函数中,如果你启用了DMA模式,传输完成的回调会从DMA中断函数里被调用。如果你在回调函数里用了FreeRTOS的信号量给任务发通知,那么中断优先级必须满足上述规则,这也就解释了为什么优先级设置错误会直接导致死锁或HardFault。

3.4 第四步:处理D-Cache与DMA缓冲区位置

这一步是H7系列绕不开的坎。很多H7工程默认开启了D-Cache,而D-Cache和DMA之间有一条著名的规则:DMA写入的内存,CPU不一定能立即看到;CPU写入的内存,DMA也不一定能立即看到。如果不做Cache维护或把缓冲区配置成non-cacheable,SD卡数据就会出现随机性错误。

结合前面讲到的DTCM问题,我的处理方案是双管齐下:

第一,把SD卡的DMA缓冲区放在DMA可访问区域。我在工程里定义了一个全局缓冲区:

__attribute__((section(".sram1"))) uint8_t sd_sector_buffer[512] __attribute__((aligned(32)));

这个段属性把缓冲区放到了SRAM1(0x30000000起始),该区域DMA可以访问。链接脚本需要包含.sram1段定义,CubeMX生成的GCC链接脚本里通常有这块区域。

第二,用MPU把这块缓冲区所在区域配置为non-cacheable。

CubeMX中可以在MPU配置里添加一个Region,地址设为0x30000000,大小设为SRAM1的对应范围,属性设置为Device或Non-cacheable。如果不想改链接脚本,也可以直接在代码里用MPU配置函数:

void MPU_Config(void) { MPU_Region_InitTypeDef MPU_InitStruct = {0}; HAL_MPU_Disable(); MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = 0x30000000; MPU_InitStruct.Size = MPU_REGION_SIZE_16KB; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0; MPU_InitStruct.Number = MPU_REGION_NUMBER0; HAL_MPU_ConfigRegion(&MPU_InitStruct); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); }

注意,这个MPU配置要在系统初始化时调用,放在调度器启动之前。配置完成后,CPU访问SRAM1时不再经过Cache,DMA读写的数据就能被CPU实时看到,fatfs读写的数据一致性就有了保证。

如果你不想动MPU,另一个方案是在每次DMA读写前后手动做Cache Clean和Invalidate:

SCB_CleanDCache_by_Addr((uint32_t *)buffer, length); HAL_SD_ReadBlocks(...); SCB_InvalidateDCache_by_Addr((uint32_t *)buffer, length);

但手动维护Cache容易漏,一旦漏掉就出随机Bug,我建议最好还是用MPU配置non-cacheable区域,一劳永逸。

3.5 第五步:在diskio层补上同步机制

即便上面全部处理完,还有一个经常被忽略的问题:FatFs的diskio层在FreeRTOS环境下工作时的线程安全性。如果你只有一个任务在访问SD卡,可能侥幸不会出问题;但一旦有任务A在写日志,任务B在读取配置文件,两个任务同时调用disk_read或disk_write,SDMMC的寄存器状态就会被打乱。

我最终在diskio.c里加了一个互斥信号量封装。思路是:在SD卡初始化时创建互斥量,在disk_read、disk_write、disk_status等函数中,操作前获取互斥量,操作后释放。

static SemaphoreHandle_t sd_mutex; void SD_Mutex_Init(void) { sd_mutex = xSemaphoreCreateMutex(); } int disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { int result = RES_ERROR; if (xSemaphoreTake(sd_mutex, portMAX_DELAY) != pdTRUE) return RES_ERROR; if (pdrv == 0 && SD_Status() == 0) { // 调用HAL_SD_ReadBlocks,注意处理好缓存地址对齐 result = RES_OK; } xSemaphoreGive(sd_mutex); return result; }

这里的互斥信号量建议在SystemInit之后、调度器启动前创建。如果工程只有一个文件系统访问者,互斥锁不是必需的;但加上它,整个系统在后期扩展任务数量时会安全很多。

另外,在disk_initialize中,我还增加了重试逻辑。SD卡在极端情况下初始化会失败一次,我在HAL_SD_Init返回失败后,会重新调用三次,每次间隔100ms,这样可以大幅提高挂载成功率。

3.6 第六步:验证与其他任务共存时的稳定性

以上修改完成后,我搭建了一个典型的业务场景来验证稳定性:一个任务每隔500ms向SD卡写入一条日志,另一个任务每3秒读取一次配置文件,还有一个命令交互任务通过串口触发文件列表操作。

在这个多任务竞争的场景下,连续跑了8小时,f_mount失败的问题没有再出现。中途我还做了几十次断电重启实验,每次都能正常挂载。

为了定位哪一步起了决定性作用,我还做了几个对比测试:只改中断优先级,问题概率下降但不彻底;只改Cache配置,问题变为偶发的文件数据错误;把时基从SysTick改到TIM6之后,挂载失败概率明显下降;加上diskio层的互斥和重试后,才真正稳定。这说明这类问题往往不是单一原因,而是多个因素叠加的结果。

4. 最终可用配置与核心代码,照着改就能稳住挂载

4.1 CubeMX侧的配置清单

我现在的标准做法如下,直接在CubeMX里按这套配置生成工程,可以省掉后面大部分踩坑时间。

配置项说明
SDMMC1模式SD 4-bit Wide bus默认SDMMC1支持4位/8位,优先4位
SDMMC1时钟200MHz用于内部时钟源,实际总线频率由分频控制
SDMMC1中断优先级5必须不低于configMAX_SYSCALL_INTERRUPT_PRIORITY
DMA中断优先级5DMA传输完成中断也要注意
Timebase SourceTIM6不要把SysTick留给HAL,要和FreeRTOS分开
NVIC优先级分组4 bits preemptGroup4,FreeRTOS硬性要求
Cache开启但不信任配合MPU把SD卡buffer区域设为non-cacheable
SD卡检测无CD脚则Disable避免检测不到卡而挂载失败

4.2 diskio.c中的关键改动

在diskio.c里,我给所有底层访问函数都加了互斥保护,并且增加了初始化重试:

#define SD_INIT_RETRY_COUNT 3 int disk_initialize(BYTE pdrv) { int retry = 0; if (pdrv != 0) return RES_PARERR; if (xSemaphoreTake(sd_mutex, portMAX_DELAY) != pdTRUE) return RES_ERROR; do { if (HAL_SD_Init(&hsd) == HAL_OK) { if (HAL_SD_GetCardStatus(&hsd) == HAL_OK) { retry = SD_INIT_RETRY_COUNT; break; } } HAL_Delay(100); retry++; } while (retry < SD_INIT_RETRY_COUNT); xSemaphoreGive(sd_mutex); if (retry >= SD_INIT_RETRY_COUNT) return RES_ERROR; return RES_OK; }

这个设计要注意:在disk_initialize里加了互斥等待,而disk_read/disk_write中也加了互斥,那么F_mount在调用disk_initialize时会先去拿互斥,初始化完成后释放,然后内部再调用disk_read拿互斥,因为互斥量在disk_initialize里已经被释放,所以不会死锁。如果你用的是递归互斥量也可以,但普通互斥量只要保证不在持锁状态下调用其他需要同一把锁的函数就行。

另外一个关键点是,FatFs底层读取时传入的buff可能不是32字节对齐的,而SDMMC的DMA要求缓冲区地址按32字节对齐。FatFs内部有对齐的临时缓冲区,但如果配置了_USE_LFN或者某些特殊设置,可能会直接传入非对齐地址。我通常在disk_read里做一个拷贝中转:

#if defined(__ICCARM__) #pragma data_alignment=32 #elif defined(__GNUC__) __attribute__((aligned(32))) #endif static uint8_t aligned_buffer[512]; int disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { // 直接基于aligned_buffer做DMA读取,再拷贝到buff if ((uint32_t)buff % 32 != 0) { HAL_SD_ReadBlocks(&hsd, aligned_buffer, sector, 1, HAL_MAX_DELAY); memcpy(buff, aligned_buffer, 512); } else { HAL_SD_ReadBlocks(&hsd, buff, sector, 1, HAL_MAX_DELAY); } }

4.3 挂载时机的安排

一个很多教程没提过的细节:f_mount在哪个任务里调用、什么时候调用,直接影响成功率。我最初是放在上电后最早运行的任务里,结果发现SD卡还没完全初始化好,f_mount就开始读卡。后来我改成了:先创建一个独立的“存储服务任务”,优先级设为中等,在调度器启动后延迟200ms执行f_mount。

为什么延迟200ms?不是玄学,是给SD卡上电留出稳定时间,同时让其他任务先跑起来,避免SD卡初始化任务和系统里其他高优先级任务在启动瞬间争抢CPU,导致ACMD41的轮询过程被频繁打断。

挂载函数如下:

void Storage_Task(void *argument) { vTaskDelay(pdMS_TO_TICKS(200)); if (f_mount(&SDFatFS, "SD:", 1) != FR_OK) { // 打印错误码,或者做一次重新挂载 vTaskDelay(pdMS_TO_TICKS(500)); f_mount(&SDFatFS, "SD:", 1); } else { // 挂载成功,开始业务 } while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } }

这个200ms延时配合disk_initialize里的重试逻辑,能覆盖绝大多数卡的上电慢、响应慢问题。

4.4 几个非常容易被忽略的细节

我在这里把几个测试中踩过的细节一起列出来,都是实际代码里踩过的坑,很多甚至会让你怀疑卡坏了:

SD卡槽的CD引脚如果悬空不接,CubeMX默认会启用SDMMC的卡检测,结果HAL_SD_Init里检测不到卡。CubeMX里SD card detect配置改为Disabled,或者在HAL_SD_MspInit里把CD引脚的GPIO配置注释掉。

H7的SDMMC在全速运行时,某些卡需要一个“发ACMD6设置块长度”的过程。HAL库内部有实现,但如果你的工程使用的是老版本HAL库,需要检查这个命令是否正常执行,否则会出现读取扇区半成功半失败的情况。

如果使用FreeRTOS + FatFs时开启了CPU Cache,不要把FatFs的工作缓冲区(如SD卡驱动中的FATFS结构体)定义在DTCM或者被Cache保护的区域。FATFS结构体本身很大,默认链接到DTCM并不导致直接错误,因为CPU可以访问DTCM,只是不能DMA访问。但最安全的做法是把所有和SD卡交互的数据都放到non-cacheable区域,FATFS结构体留在普通内存即可,因为它不会被DMA直接访问。

当你的工程里同时用了SDMMC和以太网时,要注意两边DMA内存区域别重叠。H7的以太网DMA描述符也要求非Cacheable对齐,如果MPU配置的non-cacheable区域太小,或者地址冲突,两边都可能出诡异问题。

5. 这套排查思路的迁移价值:不只是SD卡,FreeRTOS下几乎所有外设都适用

5.1 从SDMMC延伸到SPI Flash、以太网的排查心法

回过头看,这次排查过程的每一步对于FreeRTOS下跑其他外设,尤其是带DMA的外设,都有直接迁移价值。

首先是时序敏感问题。不只是SD卡,SPI Nor Flash的JEDEC ID读取、以太网PHY的启动、USB设备的枚举,都有等待外部设备响应的过程。这些过程在裸机里无压力的延时或者轮询,放进RTOS后就会被任务优先级、中断抢占影响。如果某一类外设在RTOS下时好时坏,优先检查它是否有“连续发送一串命令”的初始化序列,并考虑把它做成带重试、带延时、带较高优先级的专门任务。

其次是中断优先级。在FreeRTOS环境下,任何用到DMA和中断的外设,都要检查中断优先级是否符合configMAX_SYSCALL_INTERRUPT_PRIORITY。这是FreeRTOS的硬性规则,违反它就会出现随机死锁或HardFault。我在SPI Flash、以太网LwIP的工程中都遇到过同类问题,表现形式千奇百怪,但根因都是中断里调用了FreeRTOS API而优先级不满足要求。

再次是DMA可见内存。H7的DMA访问不到DTCM,这个特性必然影响所有使用DMA的外设,包括SDMMC、SPI、ETH、ADC、UART。只要外设用了DMA,它的缓冲区就必须在DMA可访问区域,并且最好配置为non-cacheable。这个经验对所有H7系列的工程都适用。

最后是时基。RTOS用SysTick,HAL就不能再用SysTick。一个工程里绝对不能有两个实体同时抢SysTick的中断服务函数。检查方法很简单:看整个工程代码里是不是只有一处SysTick_Handler定义,而且它最终调用了xPortSysTickHandler;同时HAL_GetTick由另一个定时器驱动。这个规则在移植任何RTOS到STM32时都适用。

5.2 我个人的几条心得

调整了许多次之后,我现在的做法是:新工程只要确定要用FreeRTOS和FatFs,就直接先把时基分开、中断优先级统一、DMA缓冲区放到非Cache区域、diskio层加互斥并带重试这四件事做掉,再开始写业务逻辑。后面遇到问题,往往就不是这些基础配置出问题了,排查范围可以大幅缩小,调试效率明显提高。

一个小技巧是,在f_mount失败时不要只打印错误码,可以在disk_read和disk_initialize里临时加一个GPIO翻转或串口打印,直观看到底层调用到了哪一步。我有一次排查,就是在disk_initialize里发现HAL_SD_Init永远在超时循环里出不来,才定位到时基混乱的问题。

还有,如果你的项目里用到了多个任务访问同一张SD卡,一定要在diskio层加互斥。不加互斥出现的问题非常随机,可能是两个任务同时写导致文件系统目录项错乱,可能是某个任务读到半截数据,更奇怪的是某个任务在等待DMA中断信号量时永远等不到。加互斥后这些问题都消失了。

这套组合拳打下来,我的STM32H7板子在FreeRTOS环境下,f_mount挂载SD卡的成功率从原来的时好时坏变成了稳定通过,连续断电上百次都没有再复现。现在每当我看到有人遇到“裸机正常、上RTOS就挂”的外设问题,我基本都会建议先按这个顺序排查,大概率能解决问题。

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

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

立即咨询